tree_haver 5.0.4 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/tree_haver/backend_context.rb +28 -0
  4. data/lib/tree_haver/backend_registry.rb +19 -432
  5. data/lib/tree_haver/contracts.rb +460 -0
  6. data/lib/tree_haver/kaitai_backend.rb +30 -0
  7. data/lib/tree_haver/language_pack.rb +190 -0
  8. data/lib/tree_haver/peg_backends.rb +76 -0
  9. data/lib/tree_haver/version.rb +1 -12
  10. data/lib/tree_haver.rb +7 -1316
  11. data.tar.gz.sig +0 -0
  12. metadata +34 -245
  13. metadata.gz.sig +0 -0
  14. data/CHANGELOG.md +0 -1366
  15. data/CITATION.cff +0 -20
  16. data/CODE_OF_CONDUCT.md +0 -134
  17. data/CONTRIBUTING.md +0 -359
  18. data/FUNDING.md +0 -74
  19. data/LICENSE.txt +0 -21
  20. data/README.md +0 -2347
  21. data/REEK +0 -0
  22. data/RUBOCOP.md +0 -71
  23. data/SECURITY.md +0 -21
  24. data/lib/tree_haver/backend_api.rb +0 -349
  25. data/lib/tree_haver/backends/citrus.rb +0 -487
  26. data/lib/tree_haver/backends/ffi.rb +0 -1009
  27. data/lib/tree_haver/backends/java.rb +0 -893
  28. data/lib/tree_haver/backends/mri.rb +0 -362
  29. data/lib/tree_haver/backends/parslet.rb +0 -560
  30. data/lib/tree_haver/backends/prism.rb +0 -471
  31. data/lib/tree_haver/backends/psych.rb +0 -375
  32. data/lib/tree_haver/backends/rust.rb +0 -239
  33. data/lib/tree_haver/base/language.rb +0 -98
  34. data/lib/tree_haver/base/node.rb +0 -322
  35. data/lib/tree_haver/base/parser.rb +0 -24
  36. data/lib/tree_haver/base/point.rb +0 -48
  37. data/lib/tree_haver/base/tree.rb +0 -128
  38. data/lib/tree_haver/base.rb +0 -12
  39. data/lib/tree_haver/citrus_grammar_finder.rb +0 -218
  40. data/lib/tree_haver/compat.rb +0 -43
  41. data/lib/tree_haver/grammar_finder.rb +0 -374
  42. data/lib/tree_haver/language.rb +0 -295
  43. data/lib/tree_haver/language_registry.rb +0 -190
  44. data/lib/tree_haver/library_path_utils.rb +0 -80
  45. data/lib/tree_haver/node.rb +0 -579
  46. data/lib/tree_haver/parser.rb +0 -438
  47. data/lib/tree_haver/parslet_grammar_finder.rb +0 -224
  48. data/lib/tree_haver/path_validator.rb +0 -353
  49. data/lib/tree_haver/point.rb +0 -27
  50. data/lib/tree_haver/rspec/dependency_tags.rb +0 -1392
  51. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  52. data/lib/tree_haver/rspec.rb +0 -33
  53. data/lib/tree_haver/tree.rb +0 -258
  54. data/sig/tree_haver/backends.rbs +0 -352
  55. data/sig/tree_haver/grammar_finder.rbs +0 -29
  56. data/sig/tree_haver/path_validator.rbs +0 -32
  57. data/sig/tree_haver.rbs +0 -234
@@ -1,1392 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
-
5
- # TreeHaver RSpec Dependency Tags
6
- #
7
- # This module provides dependency detection helpers for conditional test execution
8
- # across all gems in the TreeHaver/ast-merge family. It detects which optional
9
- # dependencies are available and configures RSpec to skip tests that require
10
- # unavailable dependencies.
11
- #
12
- # @example Loading in spec_helper.rb
13
- # require "tree_haver/rspec/dependency_tags"
14
- #
15
- # @example Usage in specs
16
- # it "requires FFI", :ffi do
17
- # # This test only runs when FFI is available
18
- # end
19
- #
20
- # it "requires ruby_tree_sitter", :mri_backend do
21
- # # This test only runs when ruby_tree_sitter gem is available
22
- # end
23
- #
24
- # it "requires tree_stump", :rust_backend do
25
- # # This test only runs when tree_stump gem is available
26
- # end
27
- #
28
- # it "requires JRuby", :jruby do
29
- # # This test only runs on JRuby
30
- # end
31
- #
32
- # it "requires libtree-sitter", :libtree_sitter do
33
- # # This test only runs when libtree-sitter.so is loadable
34
- # end
35
- #
36
- # it "requires a TOML grammar", :toml_grammar do
37
- # # This test only runs when a TOML grammar library is available
38
- # end
39
- #
40
- # @example Negated tags (for testing behavior when dependencies are NOT available)
41
- # it "only runs when ruby_tree_sitter is NOT available", :not_mri_backend do
42
- # # This test only runs when ruby_tree_sitter gem is NOT available
43
- # end
44
- #
45
- # @example Backend-specific tags
46
- # it "requires Prism backend", :prism_backend do
47
- # # This test only runs when Prism is available
48
- # end
49
- #
50
- # it "requires Psych backend", :psych_backend do
51
- # # This test only runs when Psych is available
52
- # end
53
- #
54
- # it "requires Commonmarker backend", :commonmarker_backend do
55
- # # This test only runs when commonmarker gem is available
56
- # end
57
- #
58
- # it "requires Markly backend", :markly_backend do
59
- # # This test only runs when markly gem is available
60
- # end
61
- #
62
- # it "requires Citrus backend", :citrus_backend do
63
- # # This test only runs when Citrus gem is available
64
- # end
65
- #
66
- # @example Language-specific grammar tags (for *-merge gems)
67
- # it "requires tree-sitter-bash", :bash_grammar do
68
- # # This test only runs when bash grammar is available and parsing works
69
- # end
70
- #
71
- # it "requires tree-sitter-json", :json_grammar do
72
- # # This test only runs when json grammar is available and parsing works
73
- # end
74
- #
75
- # == Available Tags
76
- #
77
- # === Naming Conventions
78
- #
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.)
84
- #
85
- # === Positive Tags (run when dependency IS available)
86
- #
87
- # ==== TreeHaver Backend Tags (*_backend)
88
- #
89
- # [:ffi_backend]
90
- # FFI backend is available. Checked dynamically per-test because FFI becomes
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.
98
- #
99
- # [:mri_backend]
100
- # ruby_tree_sitter gem is available.
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
- #
107
- # [:rust_backend]
108
- # tree_stump gem is available.
109
- #
110
- # [:java_backend]
111
- # Java backend is available (requires JRuby + java-tree-sitter/jtreesitter).
112
- #
113
- # [:prism_backend]
114
- # Prism gem is available.
115
- #
116
- # [:psych_backend]
117
- # Psych is available (stdlib, should always be true).
118
- #
119
- # [:commonmarker_backend]
120
- # commonmarker gem is available.
121
- #
122
- # [:markly_backend]
123
- # markly gem is available.
124
- #
125
- # [:citrus_backend]
126
- # Citrus gem is available.
127
- #
128
- # [:rbs_backend]
129
- # RBS gem is available (official RBS parser, MRI only).
130
- #
131
- # ==== Ruby Engine Tags (*_engine)
132
- #
133
- # [:mri_engine]
134
- # Running on MRI (CRuby).
135
- #
136
- # [:jruby_engine]
137
- # Running on JRuby.
138
- #
139
- # [:truffleruby_engine]
140
- # Running on TruffleRuby.
141
- #
142
- # ==== Tree-Sitter Grammar Tags (*_grammar)
143
- #
144
- # [:libtree_sitter]
145
- # libtree-sitter.so is loadable via FFI.
146
- #
147
- # [:bash_grammar]
148
- # tree-sitter-bash grammar is available and parsing works.
149
- #
150
- # [:toml_grammar]
151
- # tree-sitter-toml grammar is available and parsing works.
152
- #
153
- # [:json_grammar]
154
- # tree-sitter-json grammar is available and parsing works.
155
- #
156
- # [:jsonc_grammar]
157
- # tree-sitter-jsonc grammar is available and parsing works.
158
- #
159
- # [:rbs_grammar]
160
- # tree-sitter-rbs grammar is available and parsing works.
161
- #
162
- # ==== Language Parsing Capability Tags (*_parsing)
163
- #
164
- # [:toml_parsing]
165
- # At least one TOML parser (tree-sitter-toml OR toml-rb/Citrus OR toml/Parslet) is available.
166
- #
167
- # [:markdown_parsing]
168
- # At least one markdown parser (commonmarker OR markly) is available.
169
- #
170
- # [:json_parsing]
171
- # At least one JSON parser (tree-sitter-json) is available.
172
- #
173
- # [:jsonc_parsing]
174
- # At least one JSONC (JSON with Comments) parser (tree-sitter-jsonc) is available.
175
- #
176
- # [:rbs_parsing]
177
- # At least one RBS parser (rbs gem OR tree-sitter-rbs) is available.
178
- #
179
- # [:native_parsing]
180
- # A native tree-sitter backend and grammar are available.
181
- #
182
- # ==== Specific Library Tags (*_gem)
183
- #
184
- # [:toml_rb_gem]
185
- # toml-rb gem is available (Citrus backend for TOML).
186
- #
187
- # [:rbs_gem]
188
- # rbs gem is available (official RBS parser, MRI only).
189
- # Note: Also available as :rbs_backend for consistency with other parser backends.
190
- #
191
- # === Negated Tags (run when dependency is NOT available)
192
- #
193
- # All positive tags have negated versions prefixed with `not_`:
194
- # - :not_mri_backend, :not_rust_backend, :not_java_backend, :not_rbs_backend, etc.
195
- # - :not_mri_engine, :not_jruby_engine, :not_truffleruby_engine
196
- # - :not_libtree_sitter, :not_bash_grammar, :not_toml_grammar, :not_rbs_grammar, etc.
197
- # - :not_toml_parsing, :not_markdown_parsing, :not_rbs_parsing
198
- # - :not_toml_rb_gem, :not_rbs_gem
199
- #
200
- # == Backend Conflict Protection
201
- #
202
- # The MRI backend (ruby_tree_sitter) and FFI backend cannot coexist in the same
203
- # process. Once MRI loads its native extension, FFI will segfault when trying
204
- # to set a language on a parser.
205
- #
206
- # This module records backend usage when checking availability. When
207
- # `mri_backend_available?` successfully loads ruby_tree_sitter, it calls
208
- # `TreeHaver.record_backend_usage(:mri)`. This allows TreeHaver's conflict
209
- # detection (`TreeHaver.conflicting_backends_for`) to properly identify when
210
- # FFI would conflict with already-loaded backends.
211
- #
212
- # @see TreeHaver.record_backend_usage
213
- # @see TreeHaver.conflicting_backends_for
214
- # @see TreeHaver::Backends::BLOCKED_BY
215
-
216
- require "tree_haver"
217
-
218
- module TreeHaver
219
- module RSpec
220
- # Dependency detection helpers for conditional test execution
221
- module DependencyTags
222
- class << self
223
- # ============================================================
224
- # Backend Selection via Environment Variables
225
- # ============================================================
226
- #
227
- # Three environment variables control backend availability:
228
- #
229
- # TREE_HAVER_BACKEND - Single backend selection (the primary one to use)
230
- # Values: auto, mri, ffi, rust, java, citrus, prism, psych, commonmarker, markly
231
- # Default: auto
232
- #
233
- # TREE_HAVER_NATIVE_BACKEND - Allow list for native backends
234
- # Values: all, none, or comma-separated list (mri, ffi, rust, java)
235
- # Default: all (empty or unset)
236
- # Example: TREE_HAVER_NATIVE_BACKEND=mri,ffi
237
- #
238
- # TREE_HAVER_RUBY_BACKEND - Allow list for pure Ruby backends
239
- # Values: all, none, or comma-separated list (citrus, prism, psych, commonmarker, markly)
240
- # Default: all (empty or unset)
241
- # Example: TREE_HAVER_RUBY_BACKEND=citrus
242
- #
243
- # This ensures tests tagged with :mri_backend only run when MRI is allowed, etc.
244
-
245
- # Get the selected backend from TREE_HAVER_BACKEND
246
- #
247
- # @return [Symbol] the selected backend (:auto if not set)
248
- def selected_backend
249
- return @selected_backend if defined?(@selected_backend)
250
- @selected_backend = TreeHaver.backend
251
- end
252
-
253
- # Get allowed native backends from TREE_HAVER_NATIVE_BACKEND
254
- #
255
- # @return [Array<Symbol>] list of allowed native backends, or [:all] or [:none]
256
- def allowed_native_backends
257
- return @allowed_native_backends if defined?(@allowed_native_backends)
258
- @allowed_native_backends = TreeHaver.allowed_native_backends
259
- end
260
-
261
- # Get allowed Ruby backends from TREE_HAVER_RUBY_BACKEND
262
- #
263
- # @return [Array<Symbol>] list of allowed Ruby backends, or [:all] or [:none]
264
- def allowed_ruby_backends
265
- return @allowed_ruby_backends if defined?(@allowed_ruby_backends)
266
- @allowed_ruby_backends = TreeHaver.allowed_ruby_backends
267
- end
268
-
269
- # Check if a specific backend is allowed based on environment variables
270
- #
271
- # Delegates to TreeHaver.backend_allowed? which handles both
272
- # TREE_HAVER_NATIVE_BACKEND and TREE_HAVER_RUBY_BACKEND.
273
- #
274
- # @param backend [Symbol] the backend to check (:mri, :ffi, :citrus, etc.)
275
- # @return [Boolean] true if the backend is allowed
276
- def backend_allowed?(backend)
277
- TreeHaver.backend_allowed?(backend)
278
- end
279
-
280
- # ============================================================
281
- # TreeHaver Backend Availability
282
- # ============================================================
283
-
284
- # Check if FFI backend is actually usable (live check, not memoized)
285
- #
286
- # This method attempts to actually use the FFI backend by loading a language.
287
- # This provides "live" validation of backend availability because:
288
- # - If FFI gem is missing, it will fail
289
- # - If MRI backend was used first, BackendConflict will be raised
290
- # - If libtree-sitter is missing, it will fail
291
- #
292
- # NOT MEMOIZED: Each call re-checks availability. This validates that
293
- # backend protection works correctly as tests run. FFI tests should run
294
- # first (via `rake spec` which runs ffi_specs then remaining_specs).
295
- #
296
- # For isolated FFI testing, use bin/rspec-ffi
297
- #
298
- # @return [Boolean] true if FFI backend is usable
299
- def ffi_available?
300
- # If TREE_HAVER_BACKEND explicitly selects a different native backend,
301
- # FFI is not available for testing
302
- return false unless backend_allowed?(:ffi)
303
-
304
- # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
305
- # (used by ts_tree_root_node, ts_node_child, ts_node_start_point, etc.)
306
- return false if truffleruby?
307
-
308
- # Try to actually use the FFI backend
309
- path = find_toml_grammar_path
310
- return false unless path && File.exist?(path)
311
-
312
- TreeHaver.with_backend(:ffi) do
313
- TreeHaver::Language.from_library(path, symbol: "tree_sitter_toml")
314
- end
315
- true
316
- rescue TreeHaver::BackendConflict, TreeHaver::NotAvailable, LoadError
317
- false
318
- rescue StandardError
319
- # Catch any other FFI-related errors (e.g., Polyglot::ForeignException)
320
- false
321
- end
322
-
323
- # Check if ruby_tree_sitter gem is available (MRI backend)
324
- #
325
- # The MRI backend only works on MRI Ruby (C extension).
326
- # When this returns true, it also records MRI backend usage with
327
- # TreeHaver.record_backend_usage(:mri). This is critical for conflict
328
- # detection - without it, FFI would not know that MRI has been loaded.
329
- #
330
- # @return [Boolean] true if ruby_tree_sitter gem is available
331
- def mri_backend_available?
332
- return @mri_backend_available if defined?(@mri_backend_available)
333
-
334
- # If TREE_HAVER_BACKEND explicitly selects a different native backend,
335
- # MRI is not available for testing
336
- return @mri_backend_available = false unless backend_allowed?(:mri)
337
-
338
- # ruby_tree_sitter is a C extension that only works on MRI
339
- return @mri_backend_available = false unless mri?
340
-
341
- @mri_backend_available = begin
342
- # Note: gem is ruby_tree_sitter but requires tree_sitter
343
- require "tree_sitter"
344
- # Record that MRI backend is now loaded - this is critical for
345
- # conflict detection with FFI backend
346
- TreeHaver.record_backend_usage(:mri)
347
- true
348
- rescue LoadError
349
- false
350
- end
351
- end
352
-
353
- # Check if FFI backend is available WITHOUT loading MRI first
354
- #
355
- # This method is primarily for backwards compatibility with the legacy
356
- # :ffi_backend_only tag. The preferred approach is to use the standard
357
- # :ffi_backend tag, which now also triggers isolated_test_mode when
358
- # used with --tag ffi_backend.
359
- #
360
- # @return [Boolean] true if FFI backend is usable in isolation
361
- # @deprecated Use :ffi_backend tag instead of :ffi_backend_only
362
- def ffi_backend_only_available?
363
- # If TREE_HAVER_BACKEND explicitly selects a different native backend,
364
- # FFI is not available for testing
365
- return false unless backend_allowed?(:ffi)
366
-
367
- # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
368
- return false if truffleruby?
369
-
370
- # Check if FFI gem is available without loading tree_sitter
371
- begin
372
- require "ffi"
373
- rescue LoadError
374
- return false
375
- end
376
-
377
- # Try to actually use the FFI backend
378
- path = find_toml_grammar_path
379
- return false unless path && File.exist?(path)
380
-
381
- TreeHaver.with_backend(:ffi) do
382
- TreeHaver::Language.from_library(path, symbol: "tree_sitter_toml")
383
- end
384
- true
385
- rescue TreeHaver::BackendConflict, TreeHaver::NotAvailable, LoadError
386
- false
387
- rescue StandardError
388
- # Catch any other FFI-related errors
389
- false
390
- end
391
-
392
- # Check if MRI backend is available WITHOUT checking FFI availability
393
- #
394
- # This is used for the :mri_backend_only tag which runs MRI tests
395
- # without triggering any FFI availability checks.
396
- #
397
- # @return [Boolean] true if MRI backend is usable
398
- def mri_backend_only_available?
399
- return @mri_backend_only_available if defined?(@mri_backend_only_available)
400
-
401
- # If TREE_HAVER_BACKEND explicitly selects a different native backend,
402
- # MRI is not available for testing
403
- return @mri_backend_only_available = false unless backend_allowed?(:mri)
404
-
405
- # ruby_tree_sitter is a C extension that only works on MRI
406
- return @mri_backend_only_available = false unless mri?
407
-
408
- @mri_backend_only_available = begin
409
- require "tree_sitter"
410
- TreeHaver.record_backend_usage(:mri)
411
- true
412
- rescue LoadError
413
- false
414
- end
415
- end
416
-
417
- # Check if tree_stump gem is available (Rust backend)
418
- #
419
- # The Rust backend only works on MRI Ruby (magnus uses MRI's C API).
420
- #
421
- # @return [Boolean] true if tree_stump gem is available
422
- def rust_backend_available?
423
- return @rust_backend_available if defined?(@rust_backend_available)
424
-
425
- # If TREE_HAVER_BACKEND explicitly selects a different native backend,
426
- # Rust is not available for testing
427
- return @rust_backend_available = false unless backend_allowed?(:rust)
428
-
429
- # tree_stump uses magnus which requires MRI's C API
430
- return @rust_backend_available = false unless mri?
431
-
432
- @rust_backend_available = begin
433
- require "tree_stump"
434
- true
435
- rescue LoadError
436
- false
437
- end
438
- end
439
-
440
- # Check if Java backend is available AND can actually load grammars
441
- #
442
- # The Java backend requires:
443
- # 1. Running on JRuby
444
- # 2. java-tree-sitter (jtreesitter) JAR available
445
- # 3. Grammars built for java-tree-sitter's Foreign Function Memory API
446
- #
447
- # Note: Standard `.so` files built for MRI's tree-sitter C bindings are NOT
448
- # compatible with java-tree-sitter. You need grammar JARs from Maven Central
449
- # or libraries specifically built for Java FFM API.
450
- #
451
- # @return [Boolean] true if Java backend is available and can load grammars
452
- def java_backend_available?
453
- return @java_backend_available if defined?(@java_backend_available)
454
-
455
- # If TREE_HAVER_BACKEND explicitly selects a different native backend,
456
- # Java is not available for testing
457
- return @java_backend_available = false unless backend_allowed?(:java)
458
-
459
- # Must be on JRuby and have java-tree-sitter classes available
460
- return @java_backend_available = false unless jruby?
461
- return @java_backend_available = false unless TreeHaver::Backends::Java.available?
462
-
463
- # Try to actually load a grammar to verify the backend works end-to-end
464
- # This catches the case where Java classes load but grammars fail
465
- # (e.g., when using MRI-built .so files on JRuby)
466
- @java_backend_available = java_grammar_loadable?
467
- end
468
-
469
- # Check if Java backend can actually load a grammar
470
- #
471
- # This does a live test by trying to load a TOML grammar via the Java backend.
472
- # It catches the common failure case where java-tree-sitter is available but
473
- # the grammar .so files are incompatible (built for MRI, not java-tree-sitter).
474
- #
475
- # @return [Boolean] true if a grammar can be loaded via Java backend
476
- # @api private
477
- def java_grammar_loadable?
478
- return false unless jruby?
479
-
480
- path = find_toml_grammar_path
481
- return false unless path && File.exist?(path)
482
-
483
- TreeHaver.with_backend(:java) do
484
- TreeHaver::Backends::Java::Language.from_library(path, symbol: "tree_sitter_toml")
485
- end
486
- true
487
- rescue TreeHaver::NotAvailable, TreeHaver::Error, LoadError
488
- false
489
- rescue StandardError
490
- # Catch any other Java-related errors
491
- false
492
- end
493
-
494
- # Check if libtree-sitter runtime library is loadable
495
- #
496
- # @return [Boolean] true if libtree-sitter.so is loadable via FFI
497
- def libtree_sitter_available?
498
- return @libtree_sitter_available if defined?(@libtree_sitter_available)
499
- @libtree_sitter_available = begin
500
- if !ffi_available?
501
- false
502
- else
503
- TreeHaver::Backends::FFI::Native.try_load!
504
- true
505
- end
506
- rescue TreeHaver::NotAvailable, LoadError
507
- false
508
- rescue StandardError
509
- # TruffleRuby raises Polyglot::ForeignException when FFI
510
- # encounters unsupported types like STRUCT_BY_VALUE
511
- false
512
- end
513
- end
514
-
515
- # Check if a TOML grammar library is available via environment variable
516
- #
517
- # @return [Boolean] true if TREE_SITTER_TOML_PATH points to an existing file
518
- def toml_grammar_available?
519
- return @toml_grammar_available if defined?(@toml_grammar_available)
520
- path = find_toml_grammar_path
521
- @toml_grammar_available = path && File.exist?(path)
522
- end
523
-
524
- # Find the path to a TOML grammar library from environment variable
525
- #
526
- # Grammar paths should be configured via TREE_SITTER_TOML_PATH environment variable.
527
- # This keeps configuration explicit and avoids magic path guessing.
528
- #
529
- # @return [String, nil] path to TOML grammar library, or nil if not found
530
- def find_toml_grammar_path
531
- # First check environment variable
532
- env_path = ENV["TREE_SITTER_TOML_PATH"]
533
- return env_path if env_path && File.exist?(env_path)
534
-
535
- # Use GrammarFinder to search standard paths
536
- finder = TreeHaver::GrammarFinder.new(:toml, validate: false)
537
- finder.find_library_path
538
- rescue StandardError
539
- # GrammarFinder might not be available or might fail
540
- nil
541
- end
542
-
543
- # ============================================================
544
- # Dynamic Backend Availability (via BackendRegistry)
545
- # ============================================================
546
- #
547
- # External gems register tags with BackendRegistry.register_tag which
548
- # dynamically defines *_available? methods on this module.
549
- #
550
- # @example External gem registers a tag
551
- # TreeHaver::BackendRegistry.register_tag(
552
- # :my_backend_backend,
553
- # category: :backend,
554
- # require_path: "my_backend/merge"
555
- # ) { MyBackend::Merge::Backend.available? }
556
- #
557
- # # The registration automatically defines:
558
- # TreeHaver::RSpec::DependencyTags.my_backend_available? # => true/false
559
- #
560
- # Built-in backends (prism, psych, citrus, parslet) have explicit methods
561
- # defined below. External backends get methods defined dynamically when
562
- # their gem calls register_tag.
563
-
564
- # Check if prism gem is available
565
- #
566
- # @return [Boolean] true if Prism is available
567
- def prism_available?
568
- return @prism_available if defined?(@prism_available)
569
- @prism_available = TreeHaver::BackendRegistry.available?(:prism)
570
- end
571
-
572
- # Check if psych is available (stdlib, should always be true)
573
- #
574
- # @return [Boolean] true if Psych is available
575
- def psych_available?
576
- return @psych_available if defined?(@psych_available)
577
- @psych_available = TreeHaver::BackendRegistry.available?(:psych)
578
- end
579
-
580
- # Check if Citrus backend is available
581
- #
582
- # This checks if the citrus gem is installed and the backend works.
583
- #
584
- # @return [Boolean] true if Citrus backend is available
585
- def citrus_available?
586
- return @citrus_available if defined?(@citrus_available)
587
- @citrus_available = TreeHaver::BackendRegistry.available?(:citrus)
588
- end
589
-
590
- # Check if Parslet backend is available
591
- #
592
- # This checks if the parslet gem is installed and the backend works.
593
- #
594
- # @return [Boolean] true if Parslet backend is available
595
- def parslet_available?
596
- return @parslet_available if defined?(@parslet_available)
597
- @parslet_available = TreeHaver::BackendRegistry.available?(:parslet)
598
- end
599
-
600
- # ============================================================
601
- # Ruby Engine Detection
602
- # ============================================================
603
-
604
- # Check if running on JRuby
605
- #
606
- # @return [Boolean] true if running on JRuby
607
- def jruby?
608
- defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
609
- end
610
-
611
- # Check if running on TruffleRuby
612
- #
613
- # @return [Boolean] true if running on TruffleRuby
614
- def truffleruby?
615
- defined?(RUBY_ENGINE) && RUBY_ENGINE == "truffleruby"
616
- end
617
-
618
- # Check if running on MRI (CRuby)
619
- #
620
- # @return [Boolean] true if running on MRI
621
- def mri?
622
- defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
623
- end
624
-
625
- # ============================================================
626
- # Language-Specific Grammar Availability
627
- # These check that parsing actually works, not just that a grammar exists
628
- # ============================================================
629
-
630
- # Check if tree-sitter-bash grammar is available and working
631
- #
632
- # @return [Boolean] true if bash grammar works
633
- def tree_sitter_bash_available?
634
- return @tree_sitter_bash_available if defined?(@tree_sitter_bash_available)
635
- @tree_sitter_bash_available = grammar_works?(:bash, "echo hello")
636
- end
637
-
638
- # Check if tree-sitter-toml grammar is available and working via TreeHaver
639
- #
640
- # @return [Boolean] true if toml grammar works
641
- def tree_sitter_toml_available?
642
- return @tree_sitter_toml_available if defined?(@tree_sitter_toml_available)
643
- @tree_sitter_toml_available = grammar_works?(:toml, 'key = "value"')
644
- end
645
-
646
- # Check if tree-sitter-json grammar is available and working
647
- #
648
- # @return [Boolean] true if json grammar works
649
- def tree_sitter_json_available?
650
- return @tree_sitter_json_available if defined?(@tree_sitter_json_available)
651
- @tree_sitter_json_available = grammar_works?(:json, '{"key": "value"}')
652
- end
653
-
654
- # Check if tree-sitter-jsonc grammar is available and working
655
- #
656
- # @return [Boolean] true if jsonc grammar works
657
- def tree_sitter_jsonc_available?
658
- return @tree_sitter_jsonc_available if defined?(@tree_sitter_jsonc_available)
659
- @tree_sitter_jsonc_available = grammar_works?(:jsonc, '{"key": "value" /* comment */}')
660
- end
661
-
662
- # Check if tree-sitter-rbs grammar is available and working
663
- #
664
- # @return [Boolean] true if rbs grammar works
665
- def tree_sitter_rbs_available?
666
- return @tree_sitter_rbs_available if defined?(@tree_sitter_rbs_available)
667
- @tree_sitter_rbs_available = grammar_works?(:rbs, "class Foo end")
668
- end
669
-
670
- # Check if the RBS gem is available and functional
671
- #
672
- # The RBS gem only works on MRI Ruby (C extension).
673
- #
674
- # @return [Boolean] true if rbs gem is available and can parse RBS
675
- def rbs_gem_available?
676
- return @rbs_gem_available if defined?(@rbs_gem_available)
677
- @rbs_gem_available = begin
678
- require "rbs"
679
- # Verify it can actually parse - just requiring isn't enough
680
- buffer = ::RBS::Buffer.new(name: "test.rbs", content: "class Foo end")
681
- ::RBS::Parser.parse_signature(buffer)
682
- true
683
- rescue LoadError
684
- false
685
- rescue StandardError
686
- false
687
- end
688
- end
689
-
690
- # Alias for rbs_gem_available? - for consistency with other backends
691
- # Use :rbs_backend tag in specs for consistency with :prism_backend, :psych_backend, etc.
692
- #
693
- # @return [Boolean] true if rbs gem is available
694
- alias_method :rbs_backend_available?, :rbs_gem_available?
695
-
696
- # Check if at least one RBS backend is available
697
- #
698
- # @return [Boolean] true if any RBS backend works
699
- def any_rbs_backend_available?
700
- rbs_gem_available? || tree_sitter_rbs_available?
701
- end
702
-
703
- # Check if toml-rb gem is available and functional (Citrus backend for TOML)
704
- #
705
- # @return [Boolean] true if toml-rb gem is available and can parse TOML
706
- def toml_rb_gem_available?
707
- return @toml_rb_gem_available if defined?(@toml_rb_gem_available)
708
- @toml_rb_gem_available = begin
709
- require "toml-rb"
710
- # Verify it can actually parse - just requiring isn't enough
711
- TomlRB.parse('key = "value"')
712
- true
713
- rescue LoadError
714
- false
715
- rescue StandardError
716
- false
717
- end
718
- end
719
-
720
- # Check if toml gem is available and functional (Parslet backend for TOML)
721
- #
722
- # @return [Boolean] true if toml gem is available and can parse TOML
723
- def toml_gem_available?
724
- return @toml_gem_available if defined?(@toml_gem_available)
725
- @toml_gem_available = begin
726
- require "toml"
727
- # Verify it can actually parse - just requiring isn't enough
728
- source_toml = <<~TOML
729
- # My Information
730
- [machine]
731
- host = "localhost"
732
- TOML
733
- TOML.load(source_toml)
734
- true
735
- rescue LoadError
736
- false
737
- rescue StandardError
738
- false
739
- end
740
- end
741
-
742
- # Check if at least one TOML backend is available
743
- #
744
- # @return [Boolean] true if any TOML backend works
745
- def any_toml_backend_available?
746
- tree_sitter_toml_available? || toml_rb_gem_available? || toml_gem_available?
747
- end
748
-
749
- # Check if at least one markdown backend is available
750
- #
751
- # Uses BackendRegistry.tag_available? to check external backends that may
752
- # not have their methods defined yet (registered by external gems).
753
- #
754
- # @return [Boolean] true if any markdown backend works
755
- def any_markdown_backend_available?
756
- TreeHaver::BackendRegistry.tag_available?(:markly_backend) ||
757
- TreeHaver::BackendRegistry.tag_available?(:commonmarker_backend)
758
- end
759
-
760
- # Check if at least one JSON parsing backend is available
761
- #
762
- # Currently only tree-sitter-json is supported for JSON parsing.
763
- # Future backends (e.g., pure-Ruby JSON parsers) can be added here.
764
- #
765
- # @return [Boolean] true if any JSON parsing backend works
766
- def any_json_backend_available?
767
- tree_sitter_json_available?
768
- end
769
-
770
- # Check if at least one JSONC parsing backend is available
771
- #
772
- # Currently only tree-sitter-jsonc is supported for JSONC parsing.
773
- # Future backends (e.g., pure-Ruby JSONC parsers) can be added here.
774
- #
775
- # @return [Boolean] true if any JSONC parsing backend works
776
- def any_jsonc_backend_available?
777
- tree_sitter_jsonc_available?
778
- end
779
-
780
- def any_native_grammar_available?
781
- libtree_sitter_available? && (
782
- tree_sitter_bash_available? ||
783
- tree_sitter_toml_available? ||
784
- tree_sitter_json_available? ||
785
- tree_sitter_jsonc_available?
786
- )
787
- end
788
-
789
- # ============================================================
790
- # Summary and Reset
791
- # ============================================================
792
-
793
- # Determine which backends are blocked based on environment and ARGV
794
- #
795
- # This replicates the logic from RSpec.configure to determine blocked
796
- # backends BEFORE the RSpec.configure block has run. This is necessary
797
- # because summary may be called in a before(:suite) hook that runs
798
- # before the blocked_backends instance variable is set.
799
- #
800
- # @return [Set<Symbol>] set of blocked backend symbols
801
- def compute_blocked_backends
802
- blocked = Set.new
803
-
804
- # Check TREE_HAVER_BACKEND environment variable
805
- env_backend = ENV["TREE_HAVER_BACKEND"]
806
- if env_backend && !env_backend.empty? && env_backend != "auto"
807
- backend_sym = env_backend.to_sym
808
- TreeHaver::Backends::BLOCKED_BY[backend_sym]&.each { |blocker| blocked << blocker }
809
- end
810
-
811
- # Check ARGV for --tag options that indicate isolated backend testing
812
- ARGV.each_with_index do |arg, i|
813
- tag_value = nil
814
- if arg == "--tag" && ARGV[i + 1]
815
- tag_str = ARGV[i + 1]
816
- next if tag_str.start_with?("~")
817
- tag_value = tag_str.to_sym
818
- elsif arg.start_with?("--tag=")
819
- tag_str = arg.sub("--tag=", "")
820
- next if tag_str.start_with?("~")
821
- tag_value = tag_str.to_sym
822
- end
823
-
824
- next unless tag_value
825
-
826
- # Check for standard backend tags (e.g., :ffi_backend)
827
- TreeHaver::Backends::BLOCKED_BY.each do |backend, blockers|
828
- standard_tag = :"#{backend}_backend"
829
- legacy_tag = :"#{backend}_backend_only"
830
- if tag_value == standard_tag || tag_value == legacy_tag
831
- blockers.each { |blocker| blocked << blocker }
832
- end
833
- end
834
- end
835
-
836
- blocked
837
- end
838
-
839
- # Get a summary of available dependencies (for debugging)
840
- #
841
- # This method respects blocked_backends to avoid loading backends
842
- # that would conflict with isolated test modes (e.g., FFI-only tests).
843
- #
844
- # @return [Hash{Symbol => Boolean}] map of dependency name to availability
845
- def summary
846
- # Use stored blocked_backends if available, otherwise compute dynamically
847
- blocked = @blocked_backends || compute_blocked_backends
848
-
849
- result = {
850
- # Backend selection from environment variables
851
- selected_backend: selected_backend,
852
- allowed_native_backends: allowed_native_backends,
853
- allowed_ruby_backends: allowed_ruby_backends,
854
- }
855
-
856
- # Built-in TreeHaver backends (*_backend) - skip blocked backends to avoid loading them
857
- builtin_backends = {
858
- ffi: :ffi_available?,
859
- mri: :mri_backend_available?,
860
- rust: :rust_backend_available?,
861
- java: :java_backend_available?,
862
- prism: :prism_available?,
863
- psych: :psych_available?,
864
- citrus: :citrus_available?,
865
- parslet: :parslet_available?,
866
- rbs: :rbs_backend_available?,
867
- }
868
-
869
- builtin_backends.each do |backend, method|
870
- tag = :"#{backend}_backend"
871
- result[tag] = blocked.include?(backend) ? :blocked : public_send(method)
872
- end
873
-
874
- # Dynamically registered backends from BackendRegistry
875
- TreeHaver::BackendRegistry.registered_tags.each do |tag_name|
876
- next if result.key?(tag_name) # Don't override built-ins
877
-
878
- meta = TreeHaver::BackendRegistry.tag_metadata(tag_name)
879
- next unless meta && meta[:category] == :backend
880
-
881
- backend = meta[:backend_name]
882
- result[tag_name] = blocked.include?(backend) ? :blocked : TreeHaver::BackendRegistry.tag_available?(tag_name)
883
- end
884
-
885
- # Ruby engines (*_engine)
886
- result[:ruby_engine] = RUBY_ENGINE
887
- result[:mri_engine] = mri?
888
- result[:jruby_engine] = jruby?
889
- result[:truffleruby_engine] = truffleruby?
890
-
891
- # Tree-sitter grammars (*_grammar) - also respect blocked backends
892
- # since grammar checks may load backends
893
- result[:libtree_sitter] = libtree_sitter_available?
894
- result[:bash_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_bash_available?
895
- result[:toml_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_toml_available?
896
- result[:json_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_json_available?
897
- result[:jsonc_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_jsonc_available?
898
- result[:rbs_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_rbs_available?
899
- result[:any_native_grammar] = blocked.include?(:mri) ? :blocked : any_native_grammar_available?
900
-
901
- # Language parsing capabilities (*_parsing)
902
- result[:toml_parsing] = any_toml_backend_available?
903
- result[:markdown_parsing] = any_markdown_backend_available?
904
- result[:json_parsing] = any_json_backend_available?
905
- result[:jsonc_parsing] = any_jsonc_backend_available?
906
- result[:rbs_parsing] = any_rbs_backend_available?
907
-
908
- # Specific libraries (*_gem)
909
- result[:toml_rb_gem] = toml_rb_gem_available?
910
- result[:toml_gem] = toml_gem_available?
911
- result[:rbs_gem] = rbs_gem_available?
912
-
913
- result
914
- end
915
-
916
- # Get environment variable summary for debugging
917
- #
918
- # @return [Hash{String => String}] relevant environment variables
919
- def env_summary
920
- {
921
- "TREE_SITTER_BASH_PATH" => ENV["TREE_SITTER_BASH_PATH"],
922
- "TREE_SITTER_TOML_PATH" => ENV["TREE_SITTER_TOML_PATH"],
923
- "TREE_SITTER_JSON_PATH" => ENV["TREE_SITTER_JSON_PATH"],
924
- "TREE_SITTER_JSONC_PATH" => ENV["TREE_SITTER_JSONC_PATH"],
925
- "TREE_SITTER_RBS_PATH" => ENV["TREE_SITTER_RBS_PATH"],
926
- "TREE_SITTER_RUNTIME_LIB" => ENV["TREE_SITTER_RUNTIME_LIB"],
927
- "TREE_HAVER_BACKEND" => ENV["TREE_HAVER_BACKEND"],
928
- "TREE_HAVER_DEBUG" => ENV["TREE_HAVER_DEBUG"],
929
- # Library paths used by tree-sitter shared libraries
930
- "LD_LIBRARY_PATH" => ENV["LD_LIBRARY_PATH"],
931
- "DYLD_LIBRARY_PATH" => ENV["DYLD_LIBRARY_PATH"],
932
- }
933
- end
934
-
935
- # Reset all memoized availability checks
936
- #
937
- # Useful in tests that need to re-check availability after mocking.
938
- # Note: This does NOT undo backend usage recording.
939
- #
940
- # @return [void]
941
- def reset!
942
- instance_variables.each do |ivar|
943
- # Don't reset ENV-based values
944
- next if %i[@selected_backend @allowed_native_backends @allowed_ruby_backends].include?(ivar)
945
- remove_instance_variable(ivar) if ivar.to_s.end_with?("_available")
946
- end
947
- end
948
-
949
- # Reset selected backend caches (useful for testing with different ENV values)
950
- #
951
- # Also resets TreeHaver's backend caches.
952
- #
953
- # @return [void]
954
- def reset_selected_backend!
955
- remove_instance_variable(:@selected_backend) if defined?(@selected_backend)
956
- remove_instance_variable(:@allowed_native_backends) if defined?(@allowed_native_backends)
957
- remove_instance_variable(:@allowed_ruby_backends) if defined?(@allowed_ruby_backends)
958
- TreeHaver.reset_backend!
959
- end
960
-
961
- private
962
-
963
- # Generic helper to check if a grammar works by parsing test source
964
- #
965
- # @param language [Symbol] the language to test
966
- # @param test_source [String] sample source code to parse
967
- # @return [Boolean] true if parsing works without errors
968
- def grammar_works?(language, test_source)
969
- debug = !ENV.fetch("TREE_HAVER_DEBUG", "false").casecmp?("false")
970
- env_var = "TREE_SITTER_#{language.to_s.upcase}_PATH"
971
- env_value = ENV[env_var]
972
-
973
- if debug
974
- puts " [grammar_works? #{language}] ENV[#{env_var}] = #{env_value.inspect}"
975
- puts " [grammar_works? #{language}] Attempting TreeHaver.parser_for(#{language.inspect})..."
976
- end
977
-
978
- parser = TreeHaver.parser_for(language)
979
- if debug
980
- puts " [grammar_works? #{language}] Parser created: #{parser.class}"
981
- puts " [grammar_works? #{language}] Parser backend: #{parser.respond_to?(:backend) ? parser.backend : "unknown"}"
982
- end
983
-
984
- result = parser.parse(test_source)
985
- success = !result.nil? && result.root_node && !result.root_node.has_error?
986
-
987
- if debug
988
- puts " [grammar_works? #{language}] Parse result nil?: #{result.nil?}"
989
- puts " [grammar_works? #{language}] Root node: #{result&.root_node&.class}"
990
- puts " [grammar_works? #{language}] Has error?: #{result&.root_node&.has_error?}"
991
- puts " [grammar_works? #{language}] Success: #{success}"
992
- end
993
-
994
- success
995
- rescue TreeHaver::NotAvailable, TreeHaver::Error, StandardError => e
996
- if debug
997
- puts " [grammar_works? #{language}] Exception: #{e.class}: #{e.message}"
998
- puts " [grammar_works? #{language}] Returning false"
999
- end
1000
- false
1001
- end
1002
- end
1003
- end
1004
- end
1005
- end
1006
-
1007
- # NOTE: Availability methods for dynamically registered backends (like markly, commonmarker)
1008
- # are defined by BackendRegistry.define_availability_method when the backend registers via
1009
- # register_tag. This happens automatically when gems like markly-merge load, AFTER this file
1010
- # has been required. The define_availability_method in BackendRegistry checks if DependencyTags
1011
- # is loaded and defines the *_available? method at registration time.
1012
- #
1013
- # Example flow for markly-merge:
1014
- # 1. spec_helper loads tree_haver/rspec (this file) - DependencyTags module now exists
1015
- # 2. spec_helper loads markly/merge - calls BackendRegistry.register_tag(:markly_backend)
1016
- # 3. register_tag calls define_availability_method(:markly, :markly_backend)
1017
- # 4. define_availability_method defines TreeHaver::RSpec::DependencyTags.markly_available?
1018
- #
1019
- # This means by the time RSpec.configure runs below, the methods are already defined.
1020
-
1021
- # Configure RSpec with dependency-based exclusion filters
1022
- RSpec.configure do |config|
1023
- deps = TreeHaver::RSpec::DependencyTags
1024
-
1025
- # Define exclusion filters for optional dependencies
1026
- # Tests tagged with these will be skipped when the dependency is not available
1027
-
1028
- # ============================================================
1029
- # Backend Protection for Test Suites
1030
- # ============================================================
1031
- #
1032
- # TreeHaver protects against backend conflicts by default (e.g., FFI cannot
1033
- # be used after MRI has been loaded because it would cause a segfault).
1034
- # This protection remains enabled in test suites to prevent crashes.
1035
- #
1036
- # If you need to test multiple incompatible backends in the same process
1037
- # (accepting the risk of segfaults), you can disable protection:
1038
- # TREE_HAVER_BACKEND_PROTECT=false bundle exec rspec
1039
- #
1040
- # Note: The recommended approach is to run separate test processes for
1041
- # incompatible backends using RSpec tags or separate CI jobs.
1042
- if ENV["TREE_HAVER_BACKEND_PROTECT"] == "false"
1043
- TreeHaver.backend_protect = false
1044
- end
1045
-
1046
- config.before(:suite) do
1047
- # Print dependency summary if TREE_HAVER_DEBUG is set
1048
- unless ENV.fetch("TREE_HAVER_DEBUG", "false").casecmp?("false")
1049
- puts "\n=== TreeHaver Environment Variables ==="
1050
- deps.env_summary.each do |var, value|
1051
- puts " #{var}: #{value.inspect}"
1052
- end
1053
-
1054
- # Only print full dependency summary if we're not running with blocked backends
1055
- # The summary calls grammar availability checks which would load blocked backends
1056
- current_blocked = TreeHaver::RSpec::DependencyTags.instance_variable_get(:@blocked_backends) || Set.new
1057
- if current_blocked.any?
1058
- puts "\n=== TreeHaver Test Dependencies (limited - running isolated tests) ==="
1059
- puts " blocked_backends: #{current_blocked.to_a.inspect}"
1060
- puts " (Skipping full summary to avoid loading blocked backends)"
1061
- else
1062
- puts "\n=== TreeHaver Test Dependencies ==="
1063
- deps.summary.each do |dep, available|
1064
- status = case available
1065
- when true then "✓ available"
1066
- when false then "✗ not available"
1067
- else available.to_s
1068
- end
1069
- puts " #{dep}: #{status}"
1070
- end
1071
- end
1072
- puts "===================================\n"
1073
- end
1074
- end
1075
-
1076
- # ============================================================
1077
- # TreeHaver Backend Tags
1078
- # ============================================================
1079
- # Tags: *_backend - require a specific TreeHaver backend to be available
1080
- #
1081
- # Native backends (load .so files):
1082
- # :ffi_backend, :mri_backend, :rust_backend, :java_backend
1083
- # Pure-Ruby backends:
1084
- # :prism_backend, :psych_backend, :commonmarker_backend, :markly_backend, :citrus_backend
1085
- #
1086
- # Isolated backend tags (for running tests without loading conflicting backends):
1087
- # :ffi_backend_only - runs FFI tests without loading MRI backend
1088
- # :mri_backend_only - runs MRI tests without checking FFI availability
1089
-
1090
- # FFI backend exclusion:
1091
- # If MRI has already been used, FFI is blocked and will never be available.
1092
- # In this case, exclude FFI tests tagged with :ffi_backend entirely rather than
1093
- # showing them as pending.
1094
- #
1095
- # NOTE: We do NOT exclude :ffi_backend_only here because the Rakefile uses
1096
- # `--tag ~ffi_backend_only` for the remaining_specs task. RSpec interprets
1097
- # `--tag ~X` as an include filter with key "~X", which conflicts with
1098
- # filter_run_excluding. Instead, :ffi_backend_only tests will be skipped
1099
- # via the before(:each) hook below when FFI is not available.
1100
- if TreeHaver.backends_used.include?(:mri)
1101
- config.filter_run_excluding(ffi_backend: true)
1102
- end
1103
-
1104
- # FFI availability is checked dynamically per-test (not at load time)
1105
- # because FFI becomes unavailable after MRI backend is used.
1106
- # When running with :ffi_backend_only tag, this hook defers to the isolated check.
1107
- config.before(:each, :ffi_backend) do |example|
1108
- # If also tagged with :ffi_backend_only, let that hook handle the check
1109
- next if example.metadata[:ffi_backend_only]
1110
-
1111
- skip "FFI backend not available (MRI backend may have been used)" unless deps.ffi_available?
1112
- end
1113
-
1114
- # ISOLATED FFI TAG: Checked dynamically but does NOT trigger mri_backend_available?
1115
- # Use this tag for tests that must run before MRI is loaded (e.g., in ffi_specs task)
1116
- config.before(:each, :ffi_backend_only) do
1117
- skip "FFI backend not available (isolated check)" unless deps.ffi_backend_only_available?
1118
- end
1119
-
1120
- # ISOLATED MRI TAG: Checked dynamically but does NOT trigger ffi_available?
1121
- # Use this tag for tests that should run without FFI interference
1122
- config.before(:each, :mri_backend_only) do
1123
- skip "MRI backend not available (isolated check)" unless deps.mri_backend_only_available?
1124
- end
1125
-
1126
- # ============================================================
1127
- # Dynamic Backend Exclusions (using BLOCKED_BY)
1128
- # ============================================================
1129
- # When running with *_backend_only tags, we skip availability checks for
1130
- # backends that would block the isolated backend. This prevents loading
1131
- # conflicting backends before isolated tests run.
1132
- #
1133
- # For example, when running with --tag ffi_backend_only:
1134
- # - FFI is blocked by [:mri] (from BLOCKED_BY)
1135
- # - So we skip mri_backend_available? to prevent loading MRI
1136
- #
1137
- # This is dynamic based on TreeHaver::Backends::BLOCKED_BY configuration.
1138
-
1139
- # Build backend maps dynamically from BackendRegistry and built-in backends
1140
- # This allows external gems to register and automatically get tag support
1141
- backend_availability_methods = {}
1142
- backend_tags = {}
1143
-
1144
- # Built-in backends (always present in tree_haver)
1145
- builtin_backends = %i[mri rust ffi java prism psych citrus parslet rbs]
1146
- builtin_backends.each do |backend|
1147
- # Special case for ffi which uses ffi_available? not ffi_backend_available?
1148
- availability_method = (backend == :ffi) ? :ffi_available? : :"#{backend}_available?"
1149
- # Special case for backends that use *_backend_available? naming
1150
- availability_method = :"#{backend}_backend_available?" if %i[mri rust java rbs].include?(backend)
1151
-
1152
- backend_availability_methods[backend] = availability_method
1153
- backend_tags[backend] = :"#{backend}_backend"
1154
- end
1155
-
1156
- # Add dynamically registered backends from BackendRegistry
1157
- # This picks up external gems like commonmarker-merge, markly-merge, etc.
1158
- TreeHaver::BackendRegistry.registered_tags.each do |tag_name|
1159
- meta = TreeHaver::BackendRegistry.tag_metadata(tag_name)
1160
- next unless meta && meta[:category] == :backend
1161
-
1162
- backend_name = meta[:backend_name]
1163
- next if backend_availability_methods.key?(backend_name) # Don't override built-ins
1164
-
1165
- backend_availability_methods[backend_name] = :"#{backend_name}_available?"
1166
- backend_tags[backend_name] = tag_name
1167
- end
1168
-
1169
- # Determine which backends should NOT have availability checked
1170
- # based on which *_backend_only tag is being run OR which backend is
1171
- # explicitly selected via TREE_HAVER_BACKEND environment variable.
1172
- blocked_backends = Set.new
1173
-
1174
- # Track whether we're in isolated test mode (running *_backend_only tags).
1175
- # This is different from just having TREE_HAVER_BACKEND set.
1176
- # In isolated mode, we skip ALL grammar checks because they might trigger
1177
- # backend loading via TreeHaver.parser_for's auto-detection.
1178
- # When just TREE_HAVER_BACKEND is set, grammar checks are fine because
1179
- # parser_for will use the selected backend, not auto-detect.
1180
- isolated_test_mode = false
1181
-
1182
- # First, check if TREE_HAVER_BACKEND explicitly selects a backend.
1183
- # If so, block all backends that would conflict with it.
1184
- # This prevents loading MRI when TREE_HAVER_BACKEND=ffi, for example.
1185
- env_backend = ENV["TREE_HAVER_BACKEND"]
1186
- if env_backend && !env_backend.empty? && env_backend != "auto"
1187
- backend_sym = env_backend.to_sym
1188
- TreeHaver::Backends::BLOCKED_BY[backend_sym]&.each { |blocker| blocked_backends << blocker }
1189
- end
1190
-
1191
- # Check which *_backend_only tags are being run and block their conflicting backends
1192
- # config.inclusion_filter contains tags passed via --tag on command line
1193
- inclusion_rules = config.inclusion_filter.rules
1194
-
1195
- # If filter.rules is empty, check ARGV directly for --tag options
1196
- # This handles the case where RSpec hasn't processed filters yet during configuration
1197
- if inclusion_rules.empty?
1198
- ARGV.each_with_index do |arg, i|
1199
- if arg == "--tag" && ARGV[i + 1]
1200
- tag_str = ARGV[i + 1]
1201
- # Skip exclusion tags (prefixed with ~) - they are NOT inclusion filters
1202
- next if tag_str.start_with?("~")
1203
- tag_value = tag_str.to_sym
1204
- inclusion_rules[tag_value] = true
1205
- elsif arg.start_with?("--tag=")
1206
- tag_str = arg.sub("--tag=", "")
1207
- # Skip exclusion tags (prefixed with ~) - they are NOT inclusion filters
1208
- next if tag_str.start_with?("~")
1209
- tag_value = tag_str.to_sym
1210
- inclusion_rules[tag_value] = true
1211
- end
1212
- end
1213
- end
1214
-
1215
- # Check if we're running isolated backend tests using standard backend tags
1216
- # When running with --tag ffi_backend (or other native backend tags), we need
1217
- # to block conflicting backends to prevent them from loading first.
1218
- # This replaces the old *_backend_only pattern with the standard *_backend tags.
1219
- TreeHaver::Backends::BLOCKED_BY.each do |backend, blockers|
1220
- # Check if we're running this backend's tests using standard tag (e.g., :ffi_backend)
1221
- standard_tag = :"#{backend}_backend"
1222
- if inclusion_rules[standard_tag]
1223
- isolated_test_mode = true
1224
- # Add all backends that would block this one
1225
- blockers.each { |blocker| blocked_backends << blocker }
1226
- end
1227
-
1228
- # Also support legacy *_backend_only tags for backwards compatibility
1229
- legacy_tag = :"#{backend}_backend_only"
1230
- if inclusion_rules[legacy_tag]
1231
- isolated_test_mode = true
1232
- blockers.each { |blocker| blocked_backends << blocker }
1233
- end
1234
- end
1235
-
1236
- # Store blocked_backends in a module variable so before(:suite) can access it
1237
- TreeHaver::RSpec::DependencyTags.instance_variable_set(:@blocked_backends, blocked_backends)
1238
- TreeHaver::RSpec::DependencyTags.instance_variable_set(:@isolated_test_mode, isolated_test_mode)
1239
-
1240
- # Now configure exclusions, skipping availability checks for blocked backends
1241
- backend_tags.each do |backend, tag|
1242
- # FFI is handled specially with before(:each) hook above
1243
- next if backend == :ffi
1244
-
1245
- # If this backend is in blocked_backends, we exclude its tests WITHOUT checking
1246
- # availability. This prevents loading a conflicting backend while still ensuring
1247
- # tests for unavailable backends are skipped.
1248
- if blocked_backends.include?(backend)
1249
- config.filter_run_excluding(tag => true)
1250
- next
1251
- end
1252
-
1253
- availability_method = backend_availability_methods[backend]
1254
- config.filter_run_excluding(tag => true) unless deps.public_send(availability_method)
1255
- end
1256
-
1257
- # ============================================================
1258
- # Ruby Engine Tags
1259
- # ============================================================
1260
- # Tags: *_engine - require a specific Ruby engine
1261
- # :mri_engine, :jruby_engine, :truffleruby_engine
1262
-
1263
- config.filter_run_excluding(mri_engine: true) unless deps.mri?
1264
- config.filter_run_excluding(jruby_engine: true) unless deps.jruby?
1265
- config.filter_run_excluding(truffleruby_engine: true) unless deps.truffleruby?
1266
-
1267
- # ============================================================
1268
- # Tree-Sitter Grammar Tags
1269
- # ============================================================
1270
- # Tags: *_grammar - require a specific tree-sitter grammar (.so file)
1271
- # :bash_grammar, :toml_grammar, :json_grammar, :jsonc_grammar, :rbs_grammar
1272
- #
1273
- # Also: :libtree_sitter - requires the libtree-sitter runtime library
1274
- #
1275
- # NOTE: When running with *_backend_only tags, we skip these checks to avoid
1276
- # loading blocked backends. The grammar checks use TreeHaver.parser_for which
1277
- # would load the default backend (MRI) and block FFI.
1278
-
1279
- # Skip grammar availability checks only when in isolated test mode.
1280
- # When TREE_HAVER_BACKEND is explicitly set (but not using *_backend_only tags),
1281
- # grammar checks are fine because TreeHaver.parser_for respects the env var.
1282
- unless isolated_test_mode
1283
- config.before(:each) do |example|
1284
- grammar_tags = {
1285
- bash_grammar: [:tree_sitter_bash_available?, "tree-sitter-bash"],
1286
- toml_grammar: [:tree_sitter_toml_available?, "tree-sitter-toml"],
1287
- json_grammar: [:tree_sitter_json_available?, "tree-sitter-json"],
1288
- jsonc_grammar: [:tree_sitter_jsonc_available?, "tree-sitter-jsonc"],
1289
- rbs_grammar: [:tree_sitter_rbs_available?, "tree-sitter-rbs"],
1290
- libtree_sitter: [:libtree_sitter_available?, "libtree-sitter"],
1291
- }
1292
-
1293
- grammar_tags.each do |tag, (method, name)|
1294
- next unless example.metadata[tag]
1295
- unless deps.public_send(method)
1296
- env_var = "TREE_SITTER_#{tag.to_s.sub("_grammar", "").upcase}_PATH"
1297
- env_var = "TREE_SITTER_RUNTIME_LIB" if tag == :libtree_sitter
1298
- skip "#{name} grammar not available. Set #{env_var} to the path of the shared library."
1299
- end
1300
- end
1301
- end
1302
- end
1303
-
1304
- # ============================================================
1305
- # Language Parsing Capability Tags
1306
- # ============================================================
1307
- # Tags: *_parsing - require ANY parser for a language (any backend that can parse it)
1308
- # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus OR toml/Parslet)
1309
- # :markdown_parsing - any Markdown parser (commonmarker OR markly)
1310
- # :rbs_parsing - any RBS parser (rbs gem OR tree-sitter-rbs)
1311
- # :native_parsing - any native tree-sitter backend + grammar
1312
- #
1313
- # NOTE: any_toml_backend_available? calls tree_sitter_toml_available? which
1314
- # triggers grammar_works? and loads MRI. Skip when running isolated tests.
1315
-
1316
- unless isolated_test_mode
1317
- config.before(:each) do |example|
1318
- parsing_tags = {
1319
- toml_parsing: [:any_toml_backend_available?, "TOML"],
1320
- markdown_parsing: [:any_markdown_backend_available?, "Markdown"],
1321
- rbs_parsing: [:any_rbs_backend_available?, "RBS"],
1322
- native_parsing: [:any_native_grammar_available?, "Native"],
1323
- }
1324
-
1325
- parsing_tags.each do |tag, (method, name)|
1326
- next unless example.metadata[tag]
1327
- unless deps.public_send(method)
1328
- skip "#{name} parsing capability not available."
1329
- end
1330
- end
1331
- end
1332
- end
1333
-
1334
- # ============================================================
1335
- # Specific Library Tags
1336
- # ============================================================
1337
- # Tags for specific gems/libraries (*_gem suffix)
1338
- # :toml_gem - the toml gem (Parslet-based TOML parser)
1339
- # :toml_rb_gem - the toml-rb gem (Citrus-based TOML parser)
1340
- # :rbs_gem - the rbs gem (official RBS parser, MRI only)
1341
- # Note: :rbs_backend is also available as an alias for :rbs_gem
1342
-
1343
- config.filter_run_excluding(toml_gem: true) unless deps.toml_gem_available?
1344
- config.filter_run_excluding(toml_rb_gem: true) unless deps.toml_rb_gem_available?
1345
- config.filter_run_excluding(rbs_gem: true) unless deps.rbs_gem_available?
1346
-
1347
- # ============================================================
1348
- # Negated Tags (run when dependency is NOT available)
1349
- # ============================================================
1350
- # Prefix: not_* - exclude tests when the dependency IS available
1351
-
1352
- # NOTE: :not_ffi_backend tag is not provided because FFI availability is dynamic.
1353
-
1354
- # TreeHaver backends - handled dynamically to respect blocked backends
1355
- backend_tags.each do |backend, tag|
1356
- next if blocked_backends.include?(backend)
1357
-
1358
- # FFI is handled specially (availability is always dynamic)
1359
- next if backend == :ffi
1360
-
1361
- negated_tag = :"not_#{tag}"
1362
- availability_method = backend_availability_methods[backend]
1363
- config.filter_run_excluding(negated_tag => true) if deps.public_send(availability_method)
1364
- end
1365
-
1366
- # Ruby engines
1367
- config.filter_run_excluding(not_mri_engine: true) if deps.mri?
1368
- config.filter_run_excluding(not_jruby_engine: true) if deps.jruby?
1369
- config.filter_run_excluding(not_truffleruby_engine: true) if deps.truffleruby?
1370
-
1371
- # Tree-sitter grammars - skip when running isolated backend tests
1372
- unless isolated_test_mode
1373
- config.filter_run_excluding(not_libtree_sitter: true) if deps.libtree_sitter_available?
1374
- config.filter_run_excluding(not_bash_grammar: true) if deps.tree_sitter_bash_available?
1375
- config.filter_run_excluding(not_toml_grammar: true) if deps.tree_sitter_toml_available?
1376
- config.filter_run_excluding(not_json_grammar: true) if deps.tree_sitter_json_available?
1377
- config.filter_run_excluding(not_jsonc_grammar: true) if deps.tree_sitter_jsonc_available?
1378
- config.filter_run_excluding(not_rbs_grammar: true) if deps.tree_sitter_rbs_available?
1379
-
1380
- # Language parsing capabilities
1381
- config.filter_run_excluding(not_toml_parsing: true) if deps.any_toml_backend_available?
1382
- config.filter_run_excluding(not_markdown_parsing: true) if deps.any_markdown_backend_available?
1383
- config.filter_run_excluding(not_json_parsing: true) if deps.any_json_backend_available?
1384
- config.filter_run_excluding(not_jsonc_parsing: true) if deps.any_jsonc_backend_available?
1385
- config.filter_run_excluding(not_rbs_parsing: true) if deps.any_rbs_backend_available?
1386
- end
1387
-
1388
- # Specific libraries
1389
- config.filter_run_excluding(not_toml_gem: true) if deps.toml_gem_available?
1390
- config.filter_run_excluding(not_toml_rb_gem: true) if deps.toml_rb_gem_available?
1391
- config.filter_run_excluding(not_rbs_gem: true) if deps.rbs_gem_available?
1392
- end