tree_haver 5.0.0 → 5.0.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 +77 -2
- data/README.md +161 -166
- data/lib/tree_haver/backend_api.rb +12 -12
- data/lib/tree_haver/backend_registry.rb +230 -7
- data/lib/tree_haver/grammar_finder.rb +7 -6
- data/lib/tree_haver/parser.rb +5 -3
- data/lib/tree_haver/rspec/dependency_tags.rb +148 -87
- data/lib/tree_haver/rspec/testable_node.rb +217 -0
- data/lib/tree_haver/rspec.rb +11 -1
- data/lib/tree_haver/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
|
@@ -234,12 +234,12 @@ module TreeHaver
|
|
|
234
234
|
private
|
|
235
235
|
|
|
236
236
|
def validate_module_methods(mod, results)
|
|
237
|
-
unless mod.
|
|
237
|
+
unless mod.singleton_class.method_defined?(:available?)
|
|
238
238
|
results[:errors] << "Missing module method: available?"
|
|
239
239
|
results[:valid] = false
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
-
unless mod.
|
|
242
|
+
unless mod.singleton_class.method_defined?(:capabilities)
|
|
243
243
|
results[:warnings] << "Missing module method: capabilities"
|
|
244
244
|
end
|
|
245
245
|
end
|
|
@@ -248,26 +248,26 @@ module TreeHaver
|
|
|
248
248
|
# from_library is REQUIRED for all backends
|
|
249
249
|
# Language-specific backends should implement it to ignore path/symbol
|
|
250
250
|
# and return their single language (for API consistency)
|
|
251
|
-
unless klass.
|
|
251
|
+
unless klass.singleton_class.method_defined?(:from_library)
|
|
252
252
|
results[:errors] << "Language missing required class method: from_library"
|
|
253
253
|
results[:valid] = false
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
# Check for optional convenience methods
|
|
257
|
-
optional_methods = LANGUAGE_OPTIONAL_CLASS_METHODS.select { |m| klass.
|
|
257
|
+
optional_methods = LANGUAGE_OPTIONAL_CLASS_METHODS.select { |m| klass.singleton_class.method_defined?(m) }
|
|
258
258
|
if optional_methods.any?
|
|
259
259
|
results[:capabilities][:language_shortcuts] = optional_methods
|
|
260
260
|
end
|
|
261
261
|
|
|
262
262
|
results[:capabilities][:language] = {
|
|
263
|
-
class_methods: LANGUAGE_CLASS_METHODS.select { |m| klass.
|
|
263
|
+
class_methods: LANGUAGE_CLASS_METHODS.select { |m| klass.singleton_class.method_defined?(m) } +
|
|
264
264
|
optional_methods,
|
|
265
265
|
}
|
|
266
266
|
end
|
|
267
267
|
|
|
268
268
|
def validate_parser(klass, results)
|
|
269
269
|
PARSER_CLASS_METHODS.each do |method|
|
|
270
|
-
unless klass.
|
|
270
|
+
unless klass.singleton_class.method_defined?(method)
|
|
271
271
|
results[:errors] << "Parser missing class method: #{method}"
|
|
272
272
|
results[:valid] = false
|
|
273
273
|
end
|
|
@@ -275,14 +275,14 @@ module TreeHaver
|
|
|
275
275
|
|
|
276
276
|
# Check instance methods by inspecting the class
|
|
277
277
|
PARSER_INSTANCE_METHODS.each do |method|
|
|
278
|
-
unless klass.
|
|
278
|
+
unless klass.method_defined?(method) || klass.private_method_defined?(method)
|
|
279
279
|
results[:errors] << "Parser missing instance method: #{method}"
|
|
280
280
|
results[:valid] = false
|
|
281
281
|
end
|
|
282
282
|
end
|
|
283
283
|
|
|
284
284
|
PARSER_OPTIONAL_METHODS.each do |method|
|
|
285
|
-
unless klass.
|
|
285
|
+
unless klass.method_defined?(method)
|
|
286
286
|
results[:warnings] << "Parser missing optional method: #{method}"
|
|
287
287
|
end
|
|
288
288
|
end
|
|
@@ -290,14 +290,14 @@ module TreeHaver
|
|
|
290
290
|
|
|
291
291
|
def validate_tree(klass, results)
|
|
292
292
|
TREE_INSTANCE_METHODS.each do |method|
|
|
293
|
-
unless klass.
|
|
293
|
+
unless klass.method_defined?(method)
|
|
294
294
|
results[:errors] << "Tree missing instance method: #{method}"
|
|
295
295
|
results[:valid] = false
|
|
296
296
|
end
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
TREE_OPTIONAL_METHODS.each do |method|
|
|
300
|
-
unless klass.
|
|
300
|
+
unless klass.method_defined?(method)
|
|
301
301
|
results[:warnings] << "Tree missing optional method: #{method}"
|
|
302
302
|
end
|
|
303
303
|
end
|
|
@@ -330,11 +330,11 @@ module TreeHaver
|
|
|
330
330
|
end
|
|
331
331
|
|
|
332
332
|
def has_method_or_alias?(klass, method)
|
|
333
|
-
return true if klass.
|
|
333
|
+
return true if klass.method_defined?(method)
|
|
334
334
|
|
|
335
335
|
# Check aliases
|
|
336
336
|
aliases = NODE_ALIASES[method] || []
|
|
337
|
-
aliases.any? { |alt| klass.
|
|
337
|
+
aliases.any? { |alt| klass.method_defined?(alt) }
|
|
338
338
|
end
|
|
339
339
|
|
|
340
340
|
def responds_to_with_aliases?(obj, method)
|
|
@@ -22,27 +22,44 @@ module TreeHaver
|
|
|
22
22
|
# - **External backends** (commonmarker-merge, markly-merge, rbs-merge) register
|
|
23
23
|
# their checkers when their backend module is loaded
|
|
24
24
|
#
|
|
25
|
+
# == Full Tag Registration
|
|
26
|
+
#
|
|
27
|
+
# External gems can register complete tag support using {register_tag}:
|
|
28
|
+
# - Tag name (e.g., :commonmarker_backend)
|
|
29
|
+
# - Category (:backend, :gem, :parsing, :grammar)
|
|
30
|
+
# - Availability checker
|
|
31
|
+
# - Optional require path for lazy loading
|
|
32
|
+
#
|
|
33
|
+
# This enables tree_haver/rspec/dependency_tags to automatically configure
|
|
34
|
+
# RSpec exclusion filters for any registered tag without hardcoded knowledge.
|
|
35
|
+
#
|
|
25
36
|
# == Thread Safety
|
|
26
37
|
#
|
|
27
38
|
# All operations are thread-safe using a Mutex for synchronization.
|
|
28
39
|
# Results are cached after first check for performance.
|
|
29
40
|
#
|
|
30
|
-
# @example Registering a backend availability checker (
|
|
41
|
+
# @example Registering a backend availability checker (simple form)
|
|
31
42
|
# # In commonmarker-merge/lib/commonmarker/merge/backend.rb
|
|
32
43
|
# TreeHaver::BackendRegistry.register_availability_checker(:commonmarker) do
|
|
33
44
|
# available?
|
|
34
45
|
# end
|
|
35
46
|
#
|
|
47
|
+
# @example Registering a full tag with require path (preferred for external gems)
|
|
48
|
+
# TreeHaver::BackendRegistry.register_tag(
|
|
49
|
+
# :commonmarker_backend,
|
|
50
|
+
# category: :backend,
|
|
51
|
+
# backend_name: :commonmarker,
|
|
52
|
+
# require_path: "commonmarker/merge"
|
|
53
|
+
# ) { Commonmarker::Merge::Backend.available? }
|
|
54
|
+
#
|
|
36
55
|
# @example Checking backend availability
|
|
37
56
|
# TreeHaver::BackendRegistry.available?(:commonmarker) # => true/false
|
|
38
57
|
# TreeHaver::BackendRegistry.available?(:markly) # => true/false
|
|
39
58
|
# TreeHaver::BackendRegistry.available?(:rbs) # => true/false
|
|
40
59
|
#
|
|
41
|
-
# @example
|
|
42
|
-
# TreeHaver::BackendRegistry.
|
|
43
|
-
#
|
|
44
|
-
# @example Getting all registered backends
|
|
45
|
-
# TreeHaver::BackendRegistry.registered_backends # => [:mri, :rust, :ffi, ...]
|
|
60
|
+
# @example Getting all registered tags
|
|
61
|
+
# TreeHaver::BackendRegistry.registered_tags # => [:commonmarker_backend, :markly_backend, ...]
|
|
62
|
+
# TreeHaver::BackendRegistry.tags_by_category(:backend) # => [...]
|
|
46
63
|
#
|
|
47
64
|
# @see TreeHaver::RSpec::DependencyTags Uses BackendRegistry for dynamic backend detection
|
|
48
65
|
# @api public
|
|
@@ -50,15 +67,83 @@ module TreeHaver
|
|
|
50
67
|
@mutex = Mutex.new
|
|
51
68
|
@availability_checkers = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
52
69
|
@availability_cache = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
70
|
+
@tag_registry = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
71
|
+
|
|
72
|
+
# Tag categories for organizing dependency tags
|
|
73
|
+
# @api private
|
|
74
|
+
CATEGORIES = %i[backend gem parsing grammar engine other].freeze
|
|
53
75
|
|
|
54
76
|
module_function
|
|
55
77
|
|
|
56
|
-
# Register
|
|
78
|
+
# Register a full dependency tag with all metadata
|
|
79
|
+
#
|
|
80
|
+
# This is the preferred method for external gems to register their availability
|
|
81
|
+
# with complete tag support. It registers both the availability checker and
|
|
82
|
+
# the tag metadata needed for RSpec configuration.
|
|
83
|
+
#
|
|
84
|
+
# When a tag is registered, this also dynamically defines a `*_available?` method
|
|
85
|
+
# on `TreeHaver::RSpec::DependencyTags` if it doesn't already exist.
|
|
86
|
+
#
|
|
87
|
+
# @param tag_name [Symbol] the RSpec tag name (e.g., :commonmarker_backend)
|
|
88
|
+
# @param category [Symbol] one of :backend, :gem, :parsing, :grammar, :engine, :other
|
|
89
|
+
# @param backend_name [Symbol, nil] the backend name for availability checks (defaults to tag without suffix)
|
|
90
|
+
# @param require_path [String, nil] optional require path to load before checking availability
|
|
91
|
+
# @param checker [#call, nil] a callable that returns true if available
|
|
92
|
+
# @yield Block form of checker (alternative to passing a callable)
|
|
93
|
+
# @yieldreturn [Boolean] true if the tag's dependency is available
|
|
94
|
+
# @return [void]
|
|
95
|
+
#
|
|
96
|
+
# @example Register a backend tag with require path
|
|
97
|
+
# TreeHaver::BackendRegistry.register_tag(
|
|
98
|
+
# :commonmarker_backend,
|
|
99
|
+
# category: :backend,
|
|
100
|
+
# require_path: "commonmarker/merge"
|
|
101
|
+
# ) { Commonmarker::Merge::Backend.available? }
|
|
102
|
+
#
|
|
103
|
+
# @example Register a gem tag
|
|
104
|
+
# TreeHaver::BackendRegistry.register_tag(
|
|
105
|
+
# :toml_gem,
|
|
106
|
+
# category: :gem,
|
|
107
|
+
# require_path: "toml"
|
|
108
|
+
# ) { defined?(TOML) }
|
|
109
|
+
def register_tag(tag_name, category:, backend_name: nil, require_path: nil, checker: nil, &block)
|
|
110
|
+
callable = checker || block
|
|
111
|
+
raise ArgumentError, "Must provide a checker callable or block" unless callable
|
|
112
|
+
raise ArgumentError, "Checker must respond to #call" unless callable.respond_to?(:call)
|
|
113
|
+
raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)
|
|
114
|
+
|
|
115
|
+
tag_sym = tag_name.to_sym
|
|
116
|
+
# Derive backend_name from tag_name if not provided (e.g., :commonmarker_backend -> :commonmarker)
|
|
117
|
+
derived_backend = backend_name || tag_sym.to_s.sub(/_backend$/, "").to_sym
|
|
118
|
+
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
@tag_registry[tag_sym] = {
|
|
121
|
+
category: category,
|
|
122
|
+
backend_name: derived_backend,
|
|
123
|
+
require_path: require_path,
|
|
124
|
+
checker: callable,
|
|
125
|
+
}
|
|
126
|
+
# Also register as availability checker for the backend name
|
|
127
|
+
@availability_checkers[derived_backend] = callable
|
|
128
|
+
# Clear caches
|
|
129
|
+
@availability_cache.delete(derived_backend)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Dynamically define the availability method on DependencyTags
|
|
133
|
+
# This happens outside the mutex to avoid potential deadlock
|
|
134
|
+
define_availability_method(derived_backend, tag_sym)
|
|
135
|
+
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Register an availability checker for a backend (simple form)
|
|
57
140
|
#
|
|
58
141
|
# The checker should be a callable (lambda/proc/block) that returns true if
|
|
59
142
|
# the backend is available and can be used. The checker is called lazily
|
|
60
143
|
# (only when {available?} is first called for this backend).
|
|
61
144
|
#
|
|
145
|
+
# For full tag support including require paths, use {register_tag} instead.
|
|
146
|
+
#
|
|
62
147
|
# @param backend_name [Symbol, String] the backend name (e.g., :commonmarker, :markly)
|
|
63
148
|
# @param checker [#call, nil] a callable that returns true if the backend is available
|
|
64
149
|
# @yield Block form of checker (alternative to passing a callable)
|
|
@@ -206,10 +291,118 @@ module TreeHaver
|
|
|
206
291
|
@mutex.synchronize do
|
|
207
292
|
@availability_checkers.clear
|
|
208
293
|
@availability_cache.clear
|
|
294
|
+
@tag_registry.clear
|
|
209
295
|
end
|
|
210
296
|
nil
|
|
211
297
|
end
|
|
212
298
|
|
|
299
|
+
# ============================================================
|
|
300
|
+
# Tag Registry Methods
|
|
301
|
+
# ============================================================
|
|
302
|
+
|
|
303
|
+
# Get all registered tag names
|
|
304
|
+
#
|
|
305
|
+
# @return [Array<Symbol>] list of registered tag names
|
|
306
|
+
#
|
|
307
|
+
# @example
|
|
308
|
+
# TreeHaver::BackendRegistry.registered_tags
|
|
309
|
+
# # => [:commonmarker_backend, :markly_backend, :toml_gem, ...]
|
|
310
|
+
def registered_tags
|
|
311
|
+
@mutex.synchronize do
|
|
312
|
+
@tag_registry.keys.dup
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Get tags filtered by category
|
|
317
|
+
#
|
|
318
|
+
# @param category [Symbol] one of :backend, :gem, :parsing, :grammar, :engine, :other
|
|
319
|
+
# @return [Array<Symbol>] list of tag names in that category
|
|
320
|
+
#
|
|
321
|
+
# @example
|
|
322
|
+
# TreeHaver::BackendRegistry.tags_by_category(:backend)
|
|
323
|
+
# # => [:commonmarker_backend, :markly_backend, :mri_backend, ...]
|
|
324
|
+
def tags_by_category(category)
|
|
325
|
+
@mutex.synchronize do
|
|
326
|
+
@tag_registry.select { |_, meta| meta[:category] == category }.keys
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Get tag metadata
|
|
331
|
+
#
|
|
332
|
+
# @param tag_name [Symbol] the tag name
|
|
333
|
+
# @return [Hash, nil] tag metadata or nil if not registered
|
|
334
|
+
#
|
|
335
|
+
# @example
|
|
336
|
+
# TreeHaver::BackendRegistry.tag_metadata(:commonmarker_backend)
|
|
337
|
+
# # => { category: :backend, backend_name: :commonmarker, require_path: "commonmarker/merge", checker: #<Proc> }
|
|
338
|
+
def tag_metadata(tag_name)
|
|
339
|
+
@mutex.synchronize do
|
|
340
|
+
@tag_registry[tag_name.to_sym]&.dup
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Check if a tag is registered
|
|
345
|
+
#
|
|
346
|
+
# @param tag_name [Symbol] the tag name
|
|
347
|
+
# @return [Boolean] true if the tag is registered
|
|
348
|
+
def tag_registered?(tag_name)
|
|
349
|
+
@mutex.synchronize do
|
|
350
|
+
@tag_registry.key?(tag_name.to_sym)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Check if a tag's dependency is available
|
|
355
|
+
#
|
|
356
|
+
# This method handles require paths: if the tag has a require_path, it will
|
|
357
|
+
# attempt to load the gem before checking availability. This enables lazy
|
|
358
|
+
# loading of external gems.
|
|
359
|
+
#
|
|
360
|
+
# @param tag_name [Symbol] the tag name to check
|
|
361
|
+
# @return [Boolean] true if the tag's dependency is available
|
|
362
|
+
#
|
|
363
|
+
# @example
|
|
364
|
+
# TreeHaver::BackendRegistry.tag_available?(:commonmarker_backend) # => true/false
|
|
365
|
+
def tag_available?(tag_name)
|
|
366
|
+
tag_sym = tag_name.to_sym
|
|
367
|
+
|
|
368
|
+
# Get tag metadata
|
|
369
|
+
meta = @mutex.synchronize { @tag_registry[tag_sym] }
|
|
370
|
+
|
|
371
|
+
# If tag not registered, check if it's a backend name with _backend suffix
|
|
372
|
+
unless meta
|
|
373
|
+
# Try to derive backend name (e.g., :commonmarker_backend -> :commonmarker)
|
|
374
|
+
backend_name = tag_sym.to_s.sub(/_backend$/, "").to_sym
|
|
375
|
+
return available?(backend_name) if backend_name != tag_sym
|
|
376
|
+
return false
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Try to load the gem if require_path is specified
|
|
380
|
+
if meta[:require_path]
|
|
381
|
+
begin
|
|
382
|
+
require meta[:require_path]
|
|
383
|
+
rescue LoadError
|
|
384
|
+
# Gem not available
|
|
385
|
+
return false
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Check availability using the backend name
|
|
390
|
+
available?(meta[:backend_name])
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Get a summary of all registered tags and their availability
|
|
394
|
+
#
|
|
395
|
+
# @return [Hash{Symbol => Boolean}] map of tag name to availability
|
|
396
|
+
#
|
|
397
|
+
# @example
|
|
398
|
+
# TreeHaver::BackendRegistry.tag_summary
|
|
399
|
+
# # => { commonmarker_backend: true, markly_backend: false, ... }
|
|
400
|
+
def tag_summary
|
|
401
|
+
@mutex.synchronize { @tag_registry.keys.dup }.each_with_object({}) do |tag, result|
|
|
402
|
+
result[tag] = tag_available?(tag)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
213
406
|
# Check a built-in TreeHaver backend
|
|
214
407
|
#
|
|
215
408
|
# Attempts to find the backend module at `TreeHaver::Backends::<Name>` and
|
|
@@ -230,5 +423,35 @@ module TreeHaver
|
|
|
230
423
|
false
|
|
231
424
|
end
|
|
232
425
|
private_class_method :check_builtin_backend
|
|
426
|
+
|
|
427
|
+
# Dynamically define an availability method on DependencyTags
|
|
428
|
+
#
|
|
429
|
+
# This creates a `*_available?` method that checks tag_available? with
|
|
430
|
+
# memoization. The method is only defined if DependencyTags is loaded
|
|
431
|
+
# and doesn't already have a method with that name.
|
|
432
|
+
#
|
|
433
|
+
# @param backend_name [Symbol] the backend name (e.g., :commonmarker)
|
|
434
|
+
# @param tag_name [Symbol] the tag name (e.g., :commonmarker_backend)
|
|
435
|
+
# @return [void]
|
|
436
|
+
# @api private
|
|
437
|
+
def define_availability_method(backend_name, tag_name)
|
|
438
|
+
method_name = :"#{backend_name}_available?"
|
|
439
|
+
|
|
440
|
+
# Only define if DependencyTags is loaded
|
|
441
|
+
return unless defined?(TreeHaver::RSpec::DependencyTags)
|
|
442
|
+
|
|
443
|
+
deps = TreeHaver::RSpec::DependencyTags
|
|
444
|
+
|
|
445
|
+
# Don't override existing methods (built-in backends have explicit methods)
|
|
446
|
+
return if deps.respond_to?(method_name)
|
|
447
|
+
|
|
448
|
+
# Define the method dynamically
|
|
449
|
+
ivar = :"@#{backend_name}_available"
|
|
450
|
+
deps.define_singleton_method(method_name) do
|
|
451
|
+
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
|
452
|
+
instance_variable_set(ivar, TreeHaver::BackendRegistry.tag_available?(tag_name))
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
private_class_method :define_availability_method
|
|
233
456
|
end
|
|
234
457
|
end
|
|
@@ -264,12 +264,13 @@ module TreeHaver
|
|
|
264
264
|
|
|
265
265
|
# Only tree-sitter backends are relevant here
|
|
266
266
|
# Non-tree-sitter backends (Citrus, Prism, Psych, etc.) don't use grammar files
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
if mod.nil? || !TREE_SITTER_BACKENDS.include?(mod)
|
|
268
|
+
false
|
|
269
|
+
else
|
|
270
|
+
# Try to instantiate a parser - this will fail if runtime isn't available
|
|
271
|
+
mod::Parser.new
|
|
272
|
+
true
|
|
273
|
+
end
|
|
273
274
|
rescue NoMethodError, LoadError, NotAvailable => _e
|
|
274
275
|
# Note: FFI::NotFoundError inherits from LoadError, so it's caught here too
|
|
275
276
|
false
|
data/lib/tree_haver/parser.rb
CHANGED
|
@@ -364,10 +364,12 @@ module TreeHaver
|
|
|
364
364
|
|
|
365
365
|
case lang.backend
|
|
366
366
|
when :mri
|
|
367
|
-
lang.to_language if lang.respond_to?(:to_language)
|
|
368
|
-
lang.inner_language if lang.respond_to?(:inner_language)
|
|
367
|
+
return lang.to_language if lang.respond_to?(:to_language)
|
|
368
|
+
return lang.inner_language if lang.respond_to?(:inner_language)
|
|
369
|
+
lang
|
|
369
370
|
when :rust
|
|
370
|
-
lang.name if lang.respond_to?(:name)
|
|
371
|
+
return lang.name if lang.respond_to?(:name)
|
|
372
|
+
lang
|
|
371
373
|
when :ffi
|
|
372
374
|
lang # FFI needs wrapper for to_ptr
|
|
373
375
|
when :java
|