tree_haver 4.0.5 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +195 -2
- data/README.md +498 -357
- data/lib/tree_haver/backend_registry.rb +230 -7
- data/lib/tree_haver/backends/citrus.rb +98 -114
- data/lib/tree_haver/backends/ffi.rb +76 -13
- data/lib/tree_haver/backends/java.rb +99 -14
- data/lib/tree_haver/backends/mri.rb +25 -1
- data/lib/tree_haver/backends/parslet.rb +560 -0
- data/lib/tree_haver/backends/prism.rb +1 -1
- data/lib/tree_haver/backends/psych.rb +1 -1
- data/lib/tree_haver/backends/rust.rb +1 -1
- data/lib/tree_haver/base/node.rb +8 -1
- data/lib/tree_haver/language.rb +44 -13
- data/lib/tree_haver/parser.rb +127 -35
- data/lib/tree_haver/parslet_grammar_finder.rb +224 -0
- data/lib/tree_haver/point.rb +6 -44
- data/lib/tree_haver/rspec/dependency_tags.rb +148 -81
- 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/lib/tree_haver.rb +100 -13
- data.tar.gz.sig +0 -0
- metadata +16 -14
- metadata.gz.sig +0 -0
|
@@ -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
|
|
@@ -45,8 +45,10 @@ module TreeHaver
|
|
|
45
45
|
@loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
46
46
|
rescue LoadError
|
|
47
47
|
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
48
|
+
# :nocov: defensive code - StandardError during require is extremely rare
|
|
48
49
|
rescue StandardError
|
|
49
50
|
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
51
|
+
# :nocov:
|
|
50
52
|
end
|
|
51
53
|
@loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
52
54
|
end
|
|
@@ -108,6 +110,22 @@ module TreeHaver
|
|
|
108
110
|
@backend = :citrus
|
|
109
111
|
end
|
|
110
112
|
|
|
113
|
+
# Get the language name
|
|
114
|
+
#
|
|
115
|
+
# Derives a name from the grammar module name.
|
|
116
|
+
#
|
|
117
|
+
# @return [Symbol] language name
|
|
118
|
+
def language_name
|
|
119
|
+
# Derive name from grammar module (e.g., TomlRB::Document -> :toml)
|
|
120
|
+
return :unknown unless @grammar_module.respond_to?(:name) && @grammar_module.name
|
|
121
|
+
|
|
122
|
+
name = @grammar_module.name.to_s.split("::").first.downcase
|
|
123
|
+
name.sub(/rb$/, "").to_sym
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Alias for language_name (API compatibility)
|
|
127
|
+
alias_method :name, :language_name
|
|
128
|
+
|
|
111
129
|
# Compare languages for equality
|
|
112
130
|
#
|
|
113
131
|
# Citrus languages are equal if they have the same backend and grammar_module.
|
|
@@ -192,23 +210,31 @@ module TreeHaver
|
|
|
192
210
|
|
|
193
211
|
# Set the grammar for this parser
|
|
194
212
|
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
213
|
+
# Accepts either a Citrus::Language wrapper or a raw Citrus grammar module.
|
|
214
|
+
# When passed a Language wrapper, extracts the grammar_module from it.
|
|
215
|
+
# When passed a raw grammar module, uses it directly.
|
|
197
216
|
#
|
|
198
|
-
#
|
|
217
|
+
# This flexibility allows both patterns:
|
|
218
|
+
# parser.language = TreeHaver::Backends::Citrus::Language.new(TomlRB::Document)
|
|
219
|
+
# parser.language = TomlRB::Document # Also works
|
|
220
|
+
#
|
|
221
|
+
# @param grammar [Language, Module] Citrus Language wrapper or grammar module
|
|
199
222
|
# @return [void]
|
|
200
|
-
# @example
|
|
201
|
-
# require "toml-rb"
|
|
202
|
-
# # TreeHaver::Parser unwraps Language.new(TomlRB::Document) to just TomlRB::Document
|
|
203
|
-
# parser.language = TomlRB::Document # Backend receives unwrapped module
|
|
204
223
|
def language=(grammar)
|
|
205
|
-
#
|
|
206
|
-
|
|
224
|
+
# Accept Language wrapper or raw grammar module
|
|
225
|
+
actual_grammar = case grammar
|
|
226
|
+
when Language
|
|
227
|
+
grammar.grammar_module
|
|
228
|
+
else
|
|
229
|
+
grammar
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
unless actual_grammar.respond_to?(:parse)
|
|
207
233
|
raise ArgumentError,
|
|
208
|
-
"Expected Citrus grammar module with parse method, " \
|
|
234
|
+
"Expected Citrus grammar module with parse method or Language wrapper, " \
|
|
209
235
|
"got #{grammar.class}"
|
|
210
236
|
end
|
|
211
|
-
@grammar =
|
|
237
|
+
@grammar = actual_grammar
|
|
212
238
|
end
|
|
213
239
|
|
|
214
240
|
# Parse source code
|
|
@@ -247,13 +273,18 @@ module TreeHaver
|
|
|
247
273
|
# Wraps a Citrus::Match (which represents the parse tree) to provide
|
|
248
274
|
# tree-sitter-compatible API.
|
|
249
275
|
#
|
|
276
|
+
# Inherits from Base::Tree to get shared methods like #errors, #warnings,
|
|
277
|
+
# #comments, #has_error?, and #inspect.
|
|
278
|
+
#
|
|
250
279
|
# @api private
|
|
251
|
-
class Tree
|
|
252
|
-
|
|
280
|
+
class Tree < TreeHaver::Base::Tree
|
|
281
|
+
# The raw Citrus::Match root
|
|
282
|
+
# @return [Citrus::Match] The root match
|
|
283
|
+
attr_reader :root_match
|
|
253
284
|
|
|
254
285
|
def initialize(root_match, source)
|
|
255
286
|
@root_match = root_match
|
|
256
|
-
|
|
287
|
+
super(root_match, source: source)
|
|
257
288
|
end
|
|
258
289
|
|
|
259
290
|
def root_node
|
|
@@ -273,22 +304,24 @@ module TreeHaver
|
|
|
273
304
|
# - matches: child matches
|
|
274
305
|
# - captures: named groups
|
|
275
306
|
#
|
|
307
|
+
# Inherits from Base::Node to get shared methods like #first_child, #last_child,
|
|
308
|
+
# #to_s, #inspect, #==, #<=>, #source_position, #start_line, #end_line, etc.
|
|
309
|
+
#
|
|
276
310
|
# Language-specific helpers can be mixed in for convenience:
|
|
277
311
|
# require "tree_haver/backends/citrus/toml_helpers"
|
|
278
312
|
# TreeHaver::Backends::Citrus::Node.include(TreeHaver::Backends::Citrus::TomlHelpers)
|
|
279
313
|
#
|
|
280
314
|
# @api private
|
|
281
|
-
class Node
|
|
282
|
-
|
|
283
|
-
include Enumerable
|
|
284
|
-
|
|
285
|
-
attr_reader :match, :source
|
|
315
|
+
class Node < TreeHaver::Base::Node
|
|
316
|
+
attr_reader :match
|
|
286
317
|
|
|
287
318
|
def initialize(match, source)
|
|
288
319
|
@match = match
|
|
289
|
-
|
|
320
|
+
super(match, source: source)
|
|
290
321
|
end
|
|
291
322
|
|
|
323
|
+
# -- Required API Methods (from Base::Node) ----------------------------
|
|
324
|
+
|
|
292
325
|
# Get node type from Citrus rule name
|
|
293
326
|
#
|
|
294
327
|
# Uses Citrus grammar introspection to dynamically determine node types.
|
|
@@ -308,6 +341,50 @@ module TreeHaver
|
|
|
308
341
|
extract_type_from_event(@match.events.first)
|
|
309
342
|
end
|
|
310
343
|
|
|
344
|
+
def start_byte
|
|
345
|
+
@match.offset
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def end_byte
|
|
349
|
+
@match.offset + @match.length
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def children
|
|
353
|
+
return [] unless @match.respond_to?(:matches)
|
|
354
|
+
@match.matches.map { |m| Node.new(m, @source) }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# -- Overridden Methods ------------------------------------------------
|
|
358
|
+
|
|
359
|
+
# Override start_point to calculate from source
|
|
360
|
+
def start_point
|
|
361
|
+
calculate_point(@match.offset)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Override end_point to calculate from source
|
|
365
|
+
def end_point
|
|
366
|
+
calculate_point(@match.offset + @match.length)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Override text to use Citrus match string
|
|
370
|
+
def text
|
|
371
|
+
@match.string
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Override child_count for efficiency (avoid building full children array)
|
|
375
|
+
def child_count
|
|
376
|
+
@match.respond_to?(:matches) ? @match.matches.size : 0
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Override child to handle negative indices properly
|
|
380
|
+
def child(index)
|
|
381
|
+
return if index.negative?
|
|
382
|
+
return unless @match.respond_to?(:matches)
|
|
383
|
+
return if index >= @match.matches.size
|
|
384
|
+
|
|
385
|
+
Node.new(@match.matches[index], @source)
|
|
386
|
+
end
|
|
387
|
+
|
|
311
388
|
# Check if this node represents a structural element vs a terminal/token
|
|
312
389
|
#
|
|
313
390
|
# Uses Citrus grammar's terminal? method to determine if this is
|
|
@@ -387,99 +464,6 @@ module TreeHaver
|
|
|
387
464
|
"unknown"
|
|
388
465
|
end
|
|
389
466
|
|
|
390
|
-
public
|
|
391
|
-
|
|
392
|
-
def start_byte
|
|
393
|
-
@match.offset
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def end_byte
|
|
397
|
-
@match.offset + @match.length
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
def start_point
|
|
401
|
-
calculate_point(@match.offset)
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
def end_point
|
|
405
|
-
calculate_point(@match.offset + @match.length)
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
# Get the 1-based line number where this node starts
|
|
409
|
-
#
|
|
410
|
-
# @return [Integer] 1-based line number
|
|
411
|
-
def start_line
|
|
412
|
-
start_point[:row] + 1
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
# Get the 1-based line number where this node ends
|
|
416
|
-
#
|
|
417
|
-
# @return [Integer] 1-based line number
|
|
418
|
-
def end_line
|
|
419
|
-
end_point[:row] + 1
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
# Get position information as a hash
|
|
423
|
-
#
|
|
424
|
-
# Returns a hash with 1-based line numbers and 0-based columns.
|
|
425
|
-
# Compatible with *-merge gems' FileAnalysisBase.
|
|
426
|
-
#
|
|
427
|
-
# @return [Hash{Symbol => Integer}] Position hash
|
|
428
|
-
def source_position
|
|
429
|
-
{
|
|
430
|
-
start_line: start_line,
|
|
431
|
-
end_line: end_line,
|
|
432
|
-
start_column: start_point[:column],
|
|
433
|
-
end_column: end_point[:column],
|
|
434
|
-
}
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
# Get the first child node
|
|
438
|
-
#
|
|
439
|
-
# @return [Node, nil] First child or nil
|
|
440
|
-
def first_child
|
|
441
|
-
child(0)
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def text
|
|
445
|
-
@match.string
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def child_count
|
|
449
|
-
@match.respond_to?(:matches) ? @match.matches.size : 0
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def child(index)
|
|
453
|
-
return unless @match.respond_to?(:matches)
|
|
454
|
-
return if index >= @match.matches.size
|
|
455
|
-
|
|
456
|
-
Node.new(@match.matches[index], @source)
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def children
|
|
460
|
-
return [] unless @match.respond_to?(:matches)
|
|
461
|
-
@match.matches.map { |m| Node.new(m, @source) }
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def each(&block)
|
|
465
|
-
return to_enum(__method__) unless block_given?
|
|
466
|
-
children.each(&block)
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
def has_error?
|
|
470
|
-
false # Citrus raises on parse error, so successful parse has no errors
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
def missing?
|
|
474
|
-
false # Citrus doesn't have the concept of missing nodes
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
def named?
|
|
478
|
-
true # Citrus matches are typically "named" in tree-sitter terminology
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
private
|
|
482
|
-
|
|
483
467
|
def calculate_point(offset)
|
|
484
468
|
return {row: 0, column: 0} if offset <= 0
|
|
485
469
|
|
|
@@ -494,7 +478,7 @@ module TreeHaver
|
|
|
494
478
|
end
|
|
495
479
|
end
|
|
496
480
|
|
|
497
|
-
# Register availability checker for RSpec dependency tags
|
|
481
|
+
# Register the availability checker for RSpec dependency tags
|
|
498
482
|
TreeHaver::BackendRegistry.register_availability_checker(:citrus) do
|
|
499
483
|
available?
|
|
500
484
|
end
|