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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +78 -1
- data/README.md +187 -26
- data/lib/tree_haver/backends/citrus.rb +3 -0
- data/lib/tree_haver/backends/commonmarker.rb +2 -1
- data/lib/tree_haver/backends/ffi.rb +51 -10
- data/lib/tree_haver/backends/java.rb +2 -1
- data/lib/tree_haver/backends/markly.rb +2 -1
- data/lib/tree_haver/backends/mri.rb +10 -0
- data/lib/tree_haver/backends/prism.rb +11 -10
- data/lib/tree_haver/backends/psych.rb +25 -0
- data/lib/tree_haver/backends/rust.rb +1 -1
- data/lib/tree_haver/grammar_finder.rb +24 -2
- data/lib/tree_haver/language_registry.rb +6 -0
- data/lib/tree_haver/node.rb +1 -34
- data/lib/tree_haver/point.rb +65 -0
- data/lib/tree_haver/rspec/dependency_tags.rb +744 -0
- data/lib/tree_haver/rspec.rb +23 -0
- data/lib/tree_haver/tree.rb +1 -1
- data/lib/tree_haver/version.rb +1 -1
- data/lib/tree_haver.rb +108 -0
- data.tar.gz.sig +0 -0
- metadata +28 -5
- metadata.gz.sig +3 -2
|
@@ -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
|