tree_haver 3.2.6 → 4.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.
@@ -41,10 +41,13 @@ module TreeHaver
41
41
  @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
42
42
  begin
43
43
  require "prism"
44
-
45
44
  @loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
46
45
  rescue LoadError
47
46
  @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
47
+ rescue StandardError
48
+ # :nocov: defensive code - StandardError during require is extremely rare
49
+ @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
50
+ # :nocov:
48
51
  end
49
52
  @loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
50
53
  end
@@ -87,58 +90,30 @@ module TreeHaver
87
90
  # @example
88
91
  # language = TreeHaver::Backends::Prism::Language.ruby
89
92
  # parser.language = language
90
- class Language
91
- include Comparable
92
-
93
- # The language name (always :ruby for Prism)
94
- # @return [Symbol]
95
- attr_reader :name
96
- alias_method :language_name, :name
97
-
98
- # The backend this language is for
99
- # @return [Symbol]
100
- attr_reader :backend
101
-
102
- # Prism parsing options
103
- # @return [Hash]
104
- attr_reader :options
105
-
93
+ class Language < TreeHaver::Base::Language
106
94
  # @param name [Symbol] language name (should be :ruby)
107
95
  # @param options [Hash] Prism parsing options (e.g., frozen_string_literal, version)
108
96
  def initialize(name = :ruby, options: {})
109
- @name = name.to_sym
110
- @backend = :prism
111
- @options = options
97
+ super(name, backend: :prism, options: options)
112
98
 
113
- unless @name == :ruby
99
+ unless self.name == :ruby
114
100
  raise TreeHaver::NotAvailable,
115
101
  "Prism only supports Ruby parsing. " \
116
102
  "Got language: #{name.inspect}"
117
103
  end
118
104
  end
119
105
 
120
- # Compare languages for equality
121
- #
122
- # Prism languages are equal if they have the same backend and options.
106
+ # Compare languages for equality by options (since name is always :ruby)
123
107
  #
124
108
  # @param other [Object] object to compare with
125
109
  # @return [Integer, nil] -1, 0, 1, or nil if not comparable
126
110
  def <=>(other)
127
- return unless other.is_a?(Language)
128
- return unless other.backend == @backend
111
+ return unless other.is_a?(TreeHaver::Base::Language)
112
+ return unless other.backend == backend
129
113
 
130
- @options.to_a.sort <=> other.options.to_a.sort
114
+ options.to_a.sort <=> other.options.to_a.sort
131
115
  end
132
116
 
133
- # Hash value for this language (for use in Sets/Hashes)
134
- # @return [Integer]
135
- def hash
136
- [@backend, @name, @options.to_a.sort].hash
137
- end
138
-
139
- # Alias eql? to ==
140
- alias_method :eql?, :==
141
-
142
117
  class << self
143
118
  # Create a Ruby language instance (convenience method)
144
119
  #
@@ -157,17 +132,14 @@ module TreeHaver
157
132
  # Load language from library path (API compatibility)
158
133
  #
159
134
  # Prism only supports Ruby, so path and symbol parameters are ignored.
160
- # This method exists for API consistency with tree-sitter backends,
161
- # allowing `TreeHaver.parser_for(:ruby)` to work regardless of backend.
162
135
  #
163
136
  # @param _path [String] Ignored - Prism doesn't load external grammars
164
- # @param symbol [String, nil] Ignored
137
+ # @param symbol [String, nil] Ignored - Prism only supports Ruby
165
138
  # @param name [String, nil] Language name hint (defaults to :ruby)
166
139
  # @return [Language] Ruby language
167
140
  # @raise [TreeHaver::NotAvailable] if requested language is not Ruby
168
141
  def from_library(_path = nil, symbol: nil, name: nil)
169
- # Derive language name from symbol if provided
170
- lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :ruby
142
+ lang_name = name || :ruby
171
143
 
172
144
  unless lang_name == :ruby
173
145
  raise TreeHaver::NotAvailable,
@@ -185,13 +157,13 @@ module TreeHaver
185
157
  # Prism parser wrapper
186
158
  #
187
159
  # Wraps Prism to provide a tree-sitter-like API for parsing Ruby code.
188
- class Parser
160
+ class Parser < TreeHaver::Base::Parser
189
161
  # Create a new Prism parser instance
190
162
  #
191
163
  # @raise [TreeHaver::NotAvailable] if prism is not available
192
164
  def initialize
165
+ super
193
166
  raise TreeHaver::NotAvailable, "prism not available" unless Prism.available?
194
- @language = nil
195
167
  @options = {}
196
168
  end
197
169
 
@@ -251,23 +223,20 @@ module TreeHaver
251
223
  # Wraps a Prism::ParseResult to provide tree-sitter-compatible API.
252
224
  #
253
225
  # @api private
254
- class Tree
226
+ class Tree < TreeHaver::Base::Tree
255
227
  # @return [::Prism::ParseResult] the underlying Prism parse result
256
228
  attr_reader :parse_result
257
229
 
258
- # @return [String] the source code
259
- attr_reader :source
260
-
261
230
  def initialize(parse_result, source)
231
+ super(parse_result, source: source)
262
232
  @parse_result = parse_result
263
- @source = source
264
233
  end
265
234
 
266
235
  # Get the root node of the parse tree
267
236
  #
268
237
  # @return [Node] wrapped root node
269
238
  def root_node
270
- Node.new(@parse_result.value, @source)
239
+ Node.new(@parse_result.value, source)
271
240
  end
272
241
 
273
242
  # Check if the parse had errors
@@ -311,13 +280,6 @@ module TreeHaver
311
280
  def data_loc
312
281
  @parse_result.data_loc
313
282
  end
314
-
315
- # Access the underlying Prism result (passthrough)
316
- #
317
- # @return [::Prism::ParseResult]
318
- def inner_tree
319
- @parse_result
320
- end
321
283
  end
322
284
 
323
285
  # Prism node wrapper
@@ -331,18 +293,9 @@ module TreeHaver
331
293
  # - Various node-specific accessors
332
294
  #
333
295
  # @api private
334
- class Node
335
- include Enumerable
336
-
337
- # @return [::Prism::Node] the underlying Prism node
338
- attr_reader :inner_node
339
-
340
- # @return [String] the source code
341
- attr_reader :source
342
-
296
+ class Node < TreeHaver::Base::Node
343
297
  def initialize(node, source)
344
- @inner_node = node
345
- @source = source
298
+ super(node, source: source)
346
299
  end
347
300
 
348
301
  # Get node type from Prism class name
@@ -352,23 +305,25 @@ module TreeHaver
352
305
  #
353
306
  # @return [String] node type in snake_case
354
307
  def type
355
- return "nil" if @inner_node.nil?
308
+ return "nil" if inner_node.nil?
356
309
 
357
310
  # Convert class name to snake_case type
358
- # ProgramNode → program_node, CallNode → call_node
359
- class_name = @inner_node.class.name.split("::").last
311
+ class_name = inner_node.class.name.split("::").last
360
312
  class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
361
313
  end
362
314
 
363
- # Alias for tree-sitter compatibility
364
- alias_method :kind, :type
315
+ # Alias for type (API compatibility)
316
+ # @return [String] node type
317
+ def kind
318
+ type
319
+ end
365
320
 
366
321
  # Get byte offset where the node starts
367
322
  #
368
323
  # @return [Integer]
369
324
  def start_byte
370
- return 0 if @inner_node.nil? || !@inner_node.respond_to?(:location)
371
- loc = @inner_node.location
325
+ return 0 if inner_node.nil? || !inner_node.respond_to?(:location)
326
+ loc = inner_node.location
372
327
  loc&.start_offset || 0
373
328
  end
374
329
 
@@ -376,227 +331,100 @@ module TreeHaver
376
331
  #
377
332
  # @return [Integer]
378
333
  def end_byte
379
- return 0 if @inner_node.nil? || !@inner_node.respond_to?(:location)
380
- loc = @inner_node.location
334
+ return 0 if inner_node.nil? || !inner_node.respond_to?(:location)
335
+ loc = inner_node.location
381
336
  loc&.end_offset || 0
382
337
  end
383
338
 
384
- # Get the start position as row/column
339
+ # Get the start position as row/column (0-based)
385
340
  #
386
- # @return [Hash{Symbol => Integer}] with :row and :column keys
341
+ # @return [Hash{Symbol => Integer}]
387
342
  def start_point
388
- return {row: 0, column: 0} if @inner_node.nil? || !@inner_node.respond_to?(:location)
389
- loc = @inner_node.location
343
+ return {row: 0, column: 0} if inner_node.nil? || !inner_node.respond_to?(:location)
344
+ loc = inner_node.location
390
345
  return {row: 0, column: 0} unless loc
391
346
 
392
- # Prism uses 1-based lines internally but we need 0-based for tree-sitter compat
393
347
  {row: (loc.start_line - 1), column: loc.start_column}
394
348
  end
395
349
 
396
- # Get the end position as row/column
350
+ # Get the end position as row/column (0-based)
397
351
  #
398
- # @return [Hash{Symbol => Integer}] with :row and :column keys
352
+ # @return [Hash{Symbol => Integer}]
399
353
  def end_point
400
- return {row: 0, column: 0} if @inner_node.nil? || !@inner_node.respond_to?(:location)
401
- loc = @inner_node.location
354
+ return {row: 0, column: 0} if inner_node.nil? || !inner_node.respond_to?(:location)
355
+ loc = inner_node.location
402
356
  return {row: 0, column: 0} unless loc
403
357
 
404
- # Prism uses 1-based lines internally but we need 0-based for tree-sitter compat
405
358
  {row: (loc.end_line - 1), column: loc.end_column}
406
359
  end
407
360
 
408
- # Get the 1-based line number where this node starts
409
- #
410
- # @return [Integer] 1-based line number
411
- def start_line
412
- return 1 if @inner_node.nil? || !@inner_node.respond_to?(:location)
413
- loc = @inner_node.location
414
- loc&.start_line || 1
415
- end
416
-
417
- # Get the 1-based line number where this node ends
418
- #
419
- # @return [Integer] 1-based line number
420
- def end_line
421
- return 1 if @inner_node.nil? || !@inner_node.respond_to?(:location)
422
- loc = @inner_node.location
423
- loc&.end_line || 1
424
- end
425
-
426
- # Get position information as a hash
427
- #
428
- # Returns a hash with 1-based line numbers and 0-based columns.
429
- # Compatible with *-merge gems' FileAnalysisBase.
361
+ # Get all child nodes
430
362
  #
431
- # @return [Hash{Symbol => Integer}] Position hash
432
- def source_position
433
- {
434
- start_line: start_line,
435
- end_line: end_line,
436
- start_column: start_point[:column],
437
- end_column: end_point[:column],
438
- }
439
- end
363
+ # @return [Array<Node>] array of wrapped child nodes
364
+ def children
365
+ return [] if inner_node.nil?
366
+ return [] unless inner_node.respond_to?(:child_nodes)
440
367
 
441
- # Get the first child node
442
- #
443
- # @return [Node, nil] First child or nil
444
- def first_child
445
- child(0)
368
+ inner_node.child_nodes.compact.map { |n| Node.new(n, source) }
446
369
  end
447
370
 
448
371
  # Get the text content of this node
449
372
  #
450
373
  # @return [String]
451
374
  def text
452
- return "" if @inner_node.nil?
375
+ return "" if inner_node.nil?
453
376
 
454
- if @inner_node.respond_to?(:slice)
455
- @inner_node.slice
456
- elsif @source
457
- @source[start_byte...end_byte] || ""
377
+ if inner_node.respond_to?(:slice)
378
+ inner_node.slice
458
379
  else
459
- ""
380
+ super
460
381
  end
461
382
  end
462
383
 
463
384
  # Alias for Prism compatibility
464
385
  alias_method :slice, :text
465
386
 
466
- # Get the number of child nodes
467
- #
468
- # @return [Integer]
469
- def child_count
470
- return 0 if @inner_node.nil?
471
- return 0 unless @inner_node.respond_to?(:child_nodes)
472
- @inner_node.child_nodes.compact.size
473
- end
474
-
475
- # Get a child node by index
476
- #
477
- # @param index [Integer] child index
478
- # @return [Node, nil] wrapped child node
479
- def child(index)
480
- return if @inner_node.nil?
481
- return unless @inner_node.respond_to?(:child_nodes)
482
-
483
- children_array = @inner_node.child_nodes.compact
484
- return if index >= children_array.size
485
-
486
- Node.new(children_array[index], @source)
487
- end
488
-
489
- # Get all child nodes
490
- #
491
- # @return [Array<Node>] array of wrapped child nodes
492
- def children
493
- return [] if @inner_node.nil?
494
- return [] unless @inner_node.respond_to?(:child_nodes)
495
-
496
- @inner_node.child_nodes.compact.map { |n| Node.new(n, @source) }
497
- end
498
-
499
- # Iterate over child nodes
500
- #
501
- # @yield [Node] each child node
502
- # @return [Enumerator, nil]
503
- def each(&block)
504
- return to_enum(__method__) unless block_given?
505
- children.each(&block)
506
- end
507
-
508
387
  # Check if this node has errors
509
388
  #
510
389
  # @return [Boolean]
511
390
  def has_error?
512
- return false if @inner_node.nil?
391
+ return false if inner_node.nil?
513
392
 
514
393
  # Check if this is an error node type
515
394
  return true if type.include?("missing") || type.include?("error")
516
395
 
517
396
  # Check children recursively (Prism error nodes are usually children)
518
- return false unless @inner_node.respond_to?(:child_nodes)
519
- @inner_node.child_nodes.compact.any? { |n| n.class.name.to_s.include?("Missing") }
397
+ return false unless inner_node.respond_to?(:child_nodes)
398
+ inner_node.child_nodes.compact.any? { |n| n.class.name.to_s.include?("Missing") }
520
399
  end
521
400
 
522
401
  # Check if this node is a "missing" node (error recovery)
523
402
  #
524
403
  # @return [Boolean]
525
404
  def missing?
526
- return false if @inner_node.nil?
405
+ return false if inner_node.nil?
527
406
  type.include?("missing")
528
407
  end
529
408
 
530
- # Check if this is a "named" node (structural vs punctuation)
531
- #
532
- # In Prism, all nodes are "named" in tree-sitter terminology
533
- # (there's no distinction between named and anonymous nodes).
534
- #
535
- # @return [Boolean]
536
- def named?
537
- true
538
- end
539
-
540
- # Check if this is a structural node
541
- #
542
- # @return [Boolean]
543
- def structural?
544
- true
545
- end
546
-
547
409
  # Get a child by field name (Prism node accessor)
548
410
  #
549
411
  # Prism nodes have specific accessors for their children.
550
- # This method tries to call that accessor.
551
412
  #
552
413
  # @param name [String, Symbol] field/accessor name
553
414
  # @return [Node, nil] wrapped child node
554
415
  def child_by_field_name(name)
555
- return if @inner_node.nil?
556
- return unless @inner_node.respond_to?(name)
416
+ return if inner_node.nil?
417
+ return unless inner_node.respond_to?(name)
557
418
 
558
- result = @inner_node.public_send(name)
419
+ result = inner_node.public_send(name)
559
420
  return if result.nil?
560
421
 
561
- # Wrap if it's a node, otherwise return nil
562
- if result.is_a?(::Prism::Node)
563
- Node.new(result, @source)
564
- end
422
+ # Wrap if it's a node
423
+ result.is_a?(::Prism::Node) ? Node.new(result, source) : nil
565
424
  end
566
425
 
567
426
  alias_method :field, :child_by_field_name
568
427
 
569
- # Get the parent node
570
- #
571
- # @raise [NotImplementedError] Prism nodes don't have parent references
572
- # @return [void]
573
- def parent
574
- raise NotImplementedError, "Prism backend does not support parent navigation"
575
- end
576
-
577
- # Get next sibling
578
- #
579
- # @raise [NotImplementedError] Prism nodes don't have sibling references
580
- # @return [void]
581
- def next_sibling
582
- raise NotImplementedError, "Prism backend does not support sibling navigation"
583
- end
584
-
585
- # Get previous sibling
586
- #
587
- # @raise [NotImplementedError] Prism nodes don't have sibling references
588
- # @return [void]
589
- def prev_sibling
590
- raise NotImplementedError, "Prism backend does not support sibling navigation"
591
- end
592
-
593
- # String representation for debugging
594
- #
595
- # @return [String]
596
- def inspect
597
- "#<#{self.class} type=#{type} bytes=#{start_byte}..#{end_byte}>"
598
- end
599
-
600
428
  # String representation
601
429
  #
602
430
  # @return [String]
@@ -610,8 +438,8 @@ module TreeHaver
610
438
  # @param include_private [Boolean] include private methods
611
439
  # @return [Boolean]
612
440
  def respond_to_missing?(method_name, include_private = false)
613
- return false if @inner_node.nil?
614
- @inner_node.respond_to?(method_name, include_private) || super
441
+ return false if inner_node.nil?
442
+ inner_node.respond_to?(method_name, include_private) || super
615
443
  end
616
444
 
617
445
  # Delegate unknown methods to the underlying Prism node
@@ -625,13 +453,18 @@ module TreeHaver
625
453
  # @param block [Proc] block to pass
626
454
  # @return [Object] result from the underlying node
627
455
  def method_missing(method_name, *args, **kwargs, &block)
628
- if @inner_node&.respond_to?(method_name)
629
- @inner_node.public_send(method_name, *args, **kwargs, &block)
456
+ if inner_node&.respond_to?(method_name)
457
+ inner_node.public_send(method_name, *args, **kwargs, &block)
630
458
  else
631
459
  super
632
460
  end
633
461
  end
634
462
  end
463
+
464
+ # Register availability checker for RSpec dependency tags
465
+ TreeHaver::BackendRegistry.register_availability_checker(:prism) do
466
+ available?
467
+ end
635
468
  end
636
469
  end
637
470
  end