tree_haver 4.0.4 → 5.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +193 -2
- data/README.md +497 -356
- data/lib/tree_haver/backends/citrus.rb +98 -114
- data/lib/tree_haver/backends/ffi.rb +257 -14
- 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/grammar_finder.rb +1 -3
- data/lib/tree_haver/language.rb +46 -21
- data/lib/tree_haver/parser.rb +129 -45
- 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 +115 -19
- data/lib/tree_haver/version.rb +1 -1
- data/lib/tree_haver.rb +100 -13
- data.tar.gz.sig +0 -0
- metadata +15 -14
- metadata.gz.sig +0 -0
|
@@ -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
|
|
@@ -17,18 +17,26 @@ module TreeHaver
|
|
|
17
17
|
#
|
|
18
18
|
# == Tree/Node Architecture
|
|
19
19
|
#
|
|
20
|
-
# This backend
|
|
21
|
-
#
|
|
20
|
+
# This backend defines raw `FFI::Tree` and `FFI::Node` wrapper classes that
|
|
21
|
+
# provide minimal FFI bindings to the tree-sitter C structs. These are **not**
|
|
22
|
+
# intended for direct use by application code.
|
|
22
23
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# - `TreeHaver::Tree#root_node` wraps raw nodes in `TreeHaver::Node`
|
|
24
|
+
# The wrapping hierarchy is:
|
|
25
|
+
# FFI::Tree/Node (raw FFI wrappers) → TreeHaver::Tree/Node → Base::Tree/Node
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
27
|
+
# When you use `TreeHaver::Parser#parse`:
|
|
28
|
+
# 1. `FFI::Parser#parse` returns an `FFI::Tree` (raw pointer wrapper)
|
|
29
|
+
# 2. `TreeHaver::Parser` wraps it in `TreeHaver::Tree` (adds source storage)
|
|
30
|
+
# 3. `TreeHaver::Tree#root_node` wraps `FFI::Node` in `TreeHaver::Node`
|
|
29
31
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
+
# The `TreeHaver::Tree` and `TreeHaver::Node` wrappers provide the full unified
|
|
33
|
+
# API including `#children`, `#text`, `#source`, `#source_position`, etc.
|
|
34
|
+
#
|
|
35
|
+
# This differs from pure-Ruby backends (Citrus, Parslet, Prism, Psych) which
|
|
36
|
+
# define Tree/Node classes that directly inherit from Base::Tree/Base::Node.
|
|
37
|
+
#
|
|
38
|
+
# @see TreeHaver::Tree The wrapper class users should interact with
|
|
39
|
+
# @see TreeHaver::Node The wrapper class users should interact with
|
|
32
40
|
# @see TreeHaver::Base::Tree Base class documenting the Tree API contract
|
|
33
41
|
# @see TreeHaver::Base::Node Base class documenting the Node API contract
|
|
34
42
|
#
|
|
@@ -79,16 +87,20 @@ module TreeHaver
|
|
|
79
87
|
@loaded = begin # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
80
88
|
# TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
|
|
81
89
|
# which tree-sitter uses extensively (ts_tree_root_node, ts_node_child, etc.)
|
|
90
|
+
# :nocov: TruffleRuby returns false early - subsequent FFI code paths unreachable on TruffleRuby
|
|
82
91
|
if RUBY_ENGINE == "truffleruby"
|
|
83
92
|
false
|
|
93
|
+
# :nocov:
|
|
84
94
|
else
|
|
85
95
|
require "ffi"
|
|
86
96
|
true
|
|
87
97
|
end
|
|
88
98
|
rescue LoadError
|
|
89
99
|
false
|
|
100
|
+
# :nocov: defensive code - StandardError during require is extremely rare
|
|
90
101
|
rescue StandardError
|
|
91
102
|
false
|
|
103
|
+
# :nocov:
|
|
92
104
|
end
|
|
93
105
|
@loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
94
106
|
end
|
|
@@ -236,7 +248,8 @@ module TreeHaver
|
|
|
236
248
|
ffi_lib(name)
|
|
237
249
|
lib_loaded = true
|
|
238
250
|
break
|
|
239
|
-
rescue
|
|
251
|
+
rescue LoadError => e
|
|
252
|
+
# Note: FFI::NotFoundError inherits from LoadError, so it's caught here too
|
|
240
253
|
last_error = e
|
|
241
254
|
end
|
|
242
255
|
|
|
@@ -267,6 +280,7 @@ module TreeHaver
|
|
|
267
280
|
attach_function(:ts_node_type, [:ts_node], :string)
|
|
268
281
|
attach_function(:ts_node_child_count, [:ts_node], :uint32)
|
|
269
282
|
attach_function(:ts_node_child, [:ts_node, :uint32], :ts_node)
|
|
283
|
+
attach_function(:ts_node_child_by_field_name, [:ts_node, :string, :uint32], :ts_node)
|
|
270
284
|
attach_function(:ts_node_start_byte, [:ts_node], :uint32)
|
|
271
285
|
attach_function(:ts_node_end_byte, [:ts_node], :uint32)
|
|
272
286
|
attach_function(:ts_node_start_point, [:ts_node], :ts_point)
|
|
@@ -276,6 +290,21 @@ module TreeHaver
|
|
|
276
290
|
attach_function(:ts_node_is_missing, [:ts_node], :bool)
|
|
277
291
|
attach_function(:ts_node_has_error, [:ts_node], :bool)
|
|
278
292
|
|
|
293
|
+
# Node navigation functions
|
|
294
|
+
attach_function(:ts_node_parent, [:ts_node], :ts_node)
|
|
295
|
+
attach_function(:ts_node_next_sibling, [:ts_node], :ts_node)
|
|
296
|
+
attach_function(:ts_node_prev_sibling, [:ts_node], :ts_node)
|
|
297
|
+
attach_function(:ts_node_next_named_sibling, [:ts_node], :ts_node)
|
|
298
|
+
attach_function(:ts_node_prev_named_sibling, [:ts_node], :ts_node)
|
|
299
|
+
attach_function(:ts_node_named_child, [:ts_node, :uint32], :ts_node)
|
|
300
|
+
attach_function(:ts_node_named_child_count, [:ts_node], :uint32)
|
|
301
|
+
|
|
302
|
+
# Descendant lookup functions
|
|
303
|
+
attach_function(:ts_node_descendant_for_byte_range, [:ts_node, :uint32, :uint32], :ts_node)
|
|
304
|
+
attach_function(:ts_node_descendant_for_point_range, [:ts_node, :ts_point, :ts_point], :ts_node)
|
|
305
|
+
attach_function(:ts_node_named_descendant_for_byte_range, [:ts_node, :uint32, :uint32], :ts_node)
|
|
306
|
+
attach_function(:ts_node_named_descendant_for_point_range, [:ts_node, :ts_point, :ts_point], :ts_node)
|
|
307
|
+
|
|
279
308
|
# Only mark as fully loaded after all attach_function calls succeed
|
|
280
309
|
@loaded = true
|
|
281
310
|
end
|
|
@@ -354,6 +383,30 @@ module TreeHaver
|
|
|
354
383
|
# Alias eql? to ==
|
|
355
384
|
alias_method :eql?, :==
|
|
356
385
|
|
|
386
|
+
# Get the language name
|
|
387
|
+
#
|
|
388
|
+
# Derives a name from the symbol or path.
|
|
389
|
+
#
|
|
390
|
+
# @return [Symbol] language name
|
|
391
|
+
def language_name
|
|
392
|
+
# Try to derive from symbol (e.g., "tree_sitter_toml" -> :toml)
|
|
393
|
+
if @symbol
|
|
394
|
+
name = @symbol.to_s.sub(/^tree_sitter_/, "")
|
|
395
|
+
return name.to_sym
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Try to derive from path (e.g., "/path/to/libtree-sitter-toml.so" -> :toml)
|
|
399
|
+
if @path
|
|
400
|
+
name = LibraryPathUtils.derive_language_name_from_path(@path)
|
|
401
|
+
return name.to_sym if name
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
:unknown
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Alias for language_name (API compatibility)
|
|
408
|
+
alias_method :name, :language_name
|
|
409
|
+
|
|
357
410
|
# Convert to FFI pointer for passing to native functions
|
|
358
411
|
#
|
|
359
412
|
# @return [FFI::Pointer]
|
|
@@ -625,10 +678,37 @@ module TreeHaver
|
|
|
625
678
|
end
|
|
626
679
|
end
|
|
627
680
|
|
|
628
|
-
# FFI-based tree-sitter node
|
|
681
|
+
# FFI-based tree-sitter node (raw backend node)
|
|
682
|
+
#
|
|
683
|
+
# This is a **raw backend node** that wraps a TSNode by-value struct from the
|
|
684
|
+
# tree-sitter C API. It provides the minimal interface needed for tree-sitter
|
|
685
|
+
# operations but is NOT intended for direct use by application code.
|
|
686
|
+
#
|
|
687
|
+
# == Architecture Note
|
|
688
|
+
#
|
|
689
|
+
# Unlike pure-Ruby backends (Citrus, Parslet, Prism, Psych) which define Node
|
|
690
|
+
# classes that inherit from `TreeHaver::Base::Node`, tree-sitter backends (MRI,
|
|
691
|
+
# Rust, FFI, Java) define raw wrapper classes that get wrapped by `TreeHaver::Node`.
|
|
692
|
+
#
|
|
693
|
+
# The wrapping hierarchy is:
|
|
694
|
+
# FFI::Node (this class) → TreeHaver::Node → Base::Node
|
|
695
|
+
#
|
|
696
|
+
# When you use `TreeHaver::Parser#parse`, the returned tree's nodes are already
|
|
697
|
+
# wrapped in `TreeHaver::Node`, which provides the full unified API including:
|
|
698
|
+
# - `#children` - Array of child nodes
|
|
699
|
+
# - `#text` - Extract text from source
|
|
700
|
+
# - `#first_child`, `#last_child` - Convenience accessors
|
|
701
|
+
# - `#start_line`, `#end_line` - 1-based line numbers
|
|
702
|
+
# - `#source_position` - Hash with position info
|
|
703
|
+
# - `#each`, `#map`, etc. - Enumerable methods
|
|
704
|
+
# - `#to_s`, `#inspect` - String representations
|
|
705
|
+
#
|
|
706
|
+
# This raw class only implements methods that require direct FFI calls to the
|
|
707
|
+
# tree-sitter C library. The wrapper adds Ruby-level conveniences.
|
|
629
708
|
#
|
|
630
|
-
#
|
|
631
|
-
#
|
|
709
|
+
# @api private
|
|
710
|
+
# @see TreeHaver::Node The wrapper class users should interact with
|
|
711
|
+
# @see TreeHaver::Base::Node The base class documenting the full Node API
|
|
632
712
|
class Node
|
|
633
713
|
include Enumerable
|
|
634
714
|
|
|
@@ -663,6 +743,25 @@ module TreeHaver
|
|
|
663
743
|
Node.new(child_node)
|
|
664
744
|
end
|
|
665
745
|
|
|
746
|
+
# Get a child node by field name
|
|
747
|
+
#
|
|
748
|
+
# Tree-sitter grammars define named fields for certain child positions.
|
|
749
|
+
# For example, in JSON, a "pair" node has "key" and "value" fields.
|
|
750
|
+
#
|
|
751
|
+
# @param field_name [String] the field name to look up
|
|
752
|
+
# @return [Node, nil] the child node, or nil if no child has that field
|
|
753
|
+
# @example Get the key from a JSON pair
|
|
754
|
+
# pair.child_by_field_name("key") #=> Node (type: "string")
|
|
755
|
+
# pair.child_by_field_name("value") #=> Node (type: "string" or "number", etc.)
|
|
756
|
+
def child_by_field_name(field_name)
|
|
757
|
+
name = String(field_name)
|
|
758
|
+
child_node = Native.ts_node_child_by_field_name(@val, name, name.bytesize)
|
|
759
|
+
# ts_node_child_by_field_name returns a null node if field not found
|
|
760
|
+
return if Native.ts_node_is_null(child_node)
|
|
761
|
+
|
|
762
|
+
Node.new(child_node)
|
|
763
|
+
end
|
|
764
|
+
|
|
666
765
|
# Get start byte offset
|
|
667
766
|
#
|
|
668
767
|
# @return [Integer]
|
|
@@ -718,6 +817,150 @@ module TreeHaver
|
|
|
718
817
|
!!Native.ts_node_is_missing(@val)
|
|
719
818
|
end
|
|
720
819
|
|
|
820
|
+
# Check if this is a named node
|
|
821
|
+
#
|
|
822
|
+
# Named nodes represent syntactic constructs (e.g., "pair", "object").
|
|
823
|
+
# Anonymous nodes represent syntax/punctuation (e.g., "{", ",").
|
|
824
|
+
#
|
|
825
|
+
# @return [Boolean] true if this is a named node
|
|
826
|
+
def named?
|
|
827
|
+
!!Native.ts_node_is_named(@val)
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
# Get the parent node
|
|
831
|
+
#
|
|
832
|
+
# @return [Node, nil] parent node or nil if this is the root
|
|
833
|
+
def parent
|
|
834
|
+
parent_node = Native.ts_node_parent(@val)
|
|
835
|
+
return if Native.ts_node_is_null(parent_node)
|
|
836
|
+
|
|
837
|
+
Node.new(parent_node)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# Get the next sibling node
|
|
841
|
+
#
|
|
842
|
+
# @return [Node, nil] next sibling or nil if none
|
|
843
|
+
def next_sibling
|
|
844
|
+
sibling = Native.ts_node_next_sibling(@val)
|
|
845
|
+
return if Native.ts_node_is_null(sibling)
|
|
846
|
+
|
|
847
|
+
Node.new(sibling)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# Get the previous sibling node
|
|
851
|
+
#
|
|
852
|
+
# @return [Node, nil] previous sibling or nil if none
|
|
853
|
+
def prev_sibling
|
|
854
|
+
sibling = Native.ts_node_prev_sibling(@val)
|
|
855
|
+
return if Native.ts_node_is_null(sibling)
|
|
856
|
+
|
|
857
|
+
Node.new(sibling)
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Get the next named sibling node
|
|
861
|
+
#
|
|
862
|
+
# @return [Node, nil] next named sibling or nil if none
|
|
863
|
+
def next_named_sibling
|
|
864
|
+
sibling = Native.ts_node_next_named_sibling(@val)
|
|
865
|
+
return if Native.ts_node_is_null(sibling)
|
|
866
|
+
|
|
867
|
+
Node.new(sibling)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# Get the previous named sibling node
|
|
871
|
+
#
|
|
872
|
+
# @return [Node, nil] previous named sibling or nil if none
|
|
873
|
+
def prev_named_sibling
|
|
874
|
+
sibling = Native.ts_node_prev_named_sibling(@val)
|
|
875
|
+
return if Native.ts_node_is_null(sibling)
|
|
876
|
+
|
|
877
|
+
Node.new(sibling)
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
# Get a named child by index
|
|
881
|
+
#
|
|
882
|
+
# @param index [Integer] named child index (0-based)
|
|
883
|
+
# @return [Node, nil] named child or nil if index out of bounds
|
|
884
|
+
def named_child(index)
|
|
885
|
+
return if index < 0 || index >= named_child_count
|
|
886
|
+
|
|
887
|
+
child_node = Native.ts_node_named_child(@val, index)
|
|
888
|
+
return if Native.ts_node_is_null(child_node)
|
|
889
|
+
|
|
890
|
+
Node.new(child_node)
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
# Get the count of named children
|
|
894
|
+
#
|
|
895
|
+
# @return [Integer] number of named children
|
|
896
|
+
def named_child_count
|
|
897
|
+
Native.ts_node_named_child_count(@val)
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# Find the smallest descendant that spans the given byte range
|
|
901
|
+
#
|
|
902
|
+
# @param start_byte [Integer] start byte offset
|
|
903
|
+
# @param end_byte [Integer] end byte offset
|
|
904
|
+
# @return [Node, nil] descendant node or nil if not found
|
|
905
|
+
def descendant_for_byte_range(start_byte, end_byte)
|
|
906
|
+
node = Native.ts_node_descendant_for_byte_range(@val, start_byte, end_byte)
|
|
907
|
+
return if Native.ts_node_is_null(node)
|
|
908
|
+
|
|
909
|
+
Node.new(node)
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
# Find the smallest named descendant that spans the given byte range
|
|
913
|
+
#
|
|
914
|
+
# @param start_byte [Integer] start byte offset
|
|
915
|
+
# @param end_byte [Integer] end byte offset
|
|
916
|
+
# @return [Node, nil] named descendant node or nil if not found
|
|
917
|
+
def named_descendant_for_byte_range(start_byte, end_byte)
|
|
918
|
+
node = Native.ts_node_named_descendant_for_byte_range(@val, start_byte, end_byte)
|
|
919
|
+
return if Native.ts_node_is_null(node)
|
|
920
|
+
|
|
921
|
+
Node.new(node)
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# Find the smallest descendant that spans the given point range
|
|
925
|
+
#
|
|
926
|
+
# @param start_point [TreeHaver::Point, Hash] start point with :row and :column
|
|
927
|
+
# @param end_point [TreeHaver::Point, Hash] end point with :row and :column
|
|
928
|
+
# @return [Node, nil] descendant node or nil if not found
|
|
929
|
+
def descendant_for_point_range(start_point, end_point)
|
|
930
|
+
start_pt = Native::TSPoint.new
|
|
931
|
+
start_pt[:row] = start_point.respond_to?(:row) ? start_point.row : start_point[:row]
|
|
932
|
+
start_pt[:column] = start_point.respond_to?(:column) ? start_point.column : start_point[:column]
|
|
933
|
+
|
|
934
|
+
end_pt = Native::TSPoint.new
|
|
935
|
+
end_pt[:row] = end_point.respond_to?(:row) ? end_point.row : end_point[:row]
|
|
936
|
+
end_pt[:column] = end_point.respond_to?(:column) ? end_point.column : end_point[:column]
|
|
937
|
+
|
|
938
|
+
node = Native.ts_node_descendant_for_point_range(@val, start_pt, end_pt)
|
|
939
|
+
return if Native.ts_node_is_null(node)
|
|
940
|
+
|
|
941
|
+
Node.new(node)
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# Find the smallest named descendant that spans the given point range
|
|
945
|
+
#
|
|
946
|
+
# @param start_point [TreeHaver::Point, Hash] start point with :row and :column
|
|
947
|
+
# @param end_point [TreeHaver::Point, Hash] end point with :row and :column
|
|
948
|
+
# @return [Node, nil] named descendant node or nil if not found
|
|
949
|
+
def named_descendant_for_point_range(start_point, end_point)
|
|
950
|
+
start_pt = Native::TSPoint.new
|
|
951
|
+
start_pt[:row] = start_point.respond_to?(:row) ? start_point.row : start_point[:row]
|
|
952
|
+
start_pt[:column] = start_point.respond_to?(:column) ? start_point.column : start_point[:column]
|
|
953
|
+
|
|
954
|
+
end_pt = Native::TSPoint.new
|
|
955
|
+
end_pt[:row] = end_point.respond_to?(:row) ? end_point.row : end_point[:row]
|
|
956
|
+
end_pt[:column] = end_point.respond_to?(:column) ? end_point.column : end_point[:column]
|
|
957
|
+
|
|
958
|
+
node = Native.ts_node_named_descendant_for_point_range(@val, start_pt, end_pt)
|
|
959
|
+
return if Native.ts_node_is_null(node)
|
|
960
|
+
|
|
961
|
+
Node.new(node)
|
|
962
|
+
end
|
|
963
|
+
|
|
721
964
|
# Iterate over child nodes
|
|
722
965
|
#
|
|
723
966
|
# @yieldparam child [Node] each child node
|
|
@@ -757,7 +1000,7 @@ module TreeHaver
|
|
|
757
1000
|
end
|
|
758
1001
|
end
|
|
759
1002
|
|
|
760
|
-
# Register availability checker for RSpec dependency tags
|
|
1003
|
+
# Register the availability checker for RSpec dependency tags
|
|
761
1004
|
TreeHaver::BackendRegistry.register_availability_checker(:ffi) do
|
|
762
1005
|
available?
|
|
763
1006
|
end
|