tree_haver 7.0.0 → 7.1.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +1959 -0
  5. data/lib/tree_haver/backend_api.rb +392 -0
  6. data/lib/tree_haver/backend_registry.rb +153 -3
  7. data/lib/tree_haver/backends/citrus.rb +489 -0
  8. data/lib/tree_haver/backends/ffi.rb +1013 -0
  9. data/lib/tree_haver/backends/java.rb +909 -0
  10. data/lib/tree_haver/backends/mri.rb +367 -0
  11. data/lib/tree_haver/backends/parslet.rb +565 -0
  12. data/lib/tree_haver/backends/prism.rb +568 -0
  13. data/lib/tree_haver/backends/psych.rb +379 -0
  14. data/lib/tree_haver/backends/rust.rb +243 -0
  15. data/lib/tree_haver/backends/tslp.rb +274 -0
  16. data/lib/tree_haver/base/comment.rb +320 -0
  17. data/lib/tree_haver/base/language.rb +98 -0
  18. data/lib/tree_haver/base/node.rb +330 -0
  19. data/lib/tree_haver/base/parser.rb +28 -0
  20. data/lib/tree_haver/base/point.rb +48 -0
  21. data/lib/tree_haver/base/tree.rb +128 -0
  22. data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
  23. data/lib/tree_haver/contracts.rb +661 -96
  24. data/lib/tree_haver/grammar_finder.rb +429 -0
  25. data/lib/tree_haver/kaitai_backend.rb +2 -2
  26. data/lib/tree_haver/language.rb +294 -0
  27. data/lib/tree_haver/language_pack.rb +17 -166
  28. data/lib/tree_haver/language_registry.rb +221 -0
  29. data/lib/tree_haver/library_path_utils.rb +80 -0
  30. data/lib/tree_haver/node.rb +588 -0
  31. data/lib/tree_haver/parser.rb +445 -0
  32. data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
  33. data/lib/tree_haver/path_validator.rb +356 -0
  34. data/lib/tree_haver/peg_backends.rb +7 -7
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +52 -0
  37. data/lib/tree_haver/rspec.rb +3 -0
  38. data/lib/tree_haver/tree.rb +267 -0
  39. data/lib/tree_haver/version.rb +5 -3
  40. data/lib/tree_haver.rb +613 -8
  41. data/sig/tree_haver.rbs +6 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +314 -13
  44. metadata.gz.sig +0 -0
@@ -0,0 +1,568 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
5
+ # Prism backend using Ruby's built-in Prism parser
6
+ #
7
+ # This backend wraps Prism, Ruby's official parser (stdlib in Ruby 3.4+,
8
+ # available as a gem for 3.2+). Unlike tree-sitter backends which are
9
+ # language-agnostic runtime parsers, Prism is specifically designed for
10
+ # parsing Ruby source code.
11
+ #
12
+ # Prism provides excellent error recovery, detailed location information,
13
+ # and is the future of Ruby parsing (used by CRuby, JRuby, TruffleRuby).
14
+ #
15
+ # @note This backend only parses Ruby source code
16
+ # @see https://github.com/ruby/prism Prism parser
17
+ #
18
+ # @example Basic usage
19
+ # parser = TreeHaver::Parser.new
20
+ # parser.language = TreeHaver::Backends::Prism::Language.ruby
21
+ # tree = parser.parse(ruby_source)
22
+ # root = tree.root_node
23
+ # puts root.type # => "program_node"
24
+ module Prism
25
+ @load_attempted = false
26
+ @loaded = false
27
+
28
+ # Check if the Prism backend is available
29
+ #
30
+ # Attempts to require prism on first call and caches the result.
31
+ # On Ruby 3.4+, Prism is in stdlib. On 3.2-3.3, it's a gem.
32
+ #
33
+ # @return [Boolean] true if prism is available
34
+ # @example
35
+ # if TreeHaver::Backends::Prism.available?
36
+ # puts "Prism backend is ready"
37
+ # end
38
+ class << self
39
+ def available?
40
+ return @loaded if @load_attempted
41
+
42
+ @load_attempted = true
43
+ begin
44
+ require 'prism'
45
+ @loaded = true
46
+ rescue LoadError
47
+ @loaded = false
48
+ rescue StandardError
49
+ # simplecov:disable defensive code - StandardError during require is extremely rare
50
+ @loaded = false
51
+ # simplecov:enable
52
+ end
53
+ @loaded
54
+ end
55
+
56
+ # Reset the load state (primarily for testing)
57
+ #
58
+ # @return [void]
59
+ # @api private
60
+ def reset!
61
+ @load_attempted = false
62
+ @loaded = false
63
+ end
64
+
65
+ # Get capabilities supported by this backend
66
+ #
67
+ # @return [Hash{Symbol => Object}] capability map
68
+ # @example
69
+ # TreeHaver::Backends::Prism.capabilities
70
+ # # => { backend: :prism, query: false, bytes_field: true, incremental: false, ruby_only: true, comment_support: :partial }
71
+ def capabilities
72
+ return {} unless available?
73
+
74
+ {
75
+ backend: :prism,
76
+ query: false, # Prism doesn't have tree-sitter-style queries (has pattern matching)
77
+ bytes_field: true, # Prism provides byte offsets via Location
78
+ incremental: false, # Prism doesn't support incremental parsing (yet)
79
+ pure_ruby: false, # Prism has native C extension (but also pure Ruby mode)
80
+ ruby_only: true, # Prism only parses Ruby source code
81
+ error_tolerant: true, # Prism has excellent error recovery
82
+ comment_support: :partial,
83
+ comment_attachment_hints: true
84
+ }
85
+ end
86
+ end
87
+
88
+ # Prism language wrapper
89
+ #
90
+ # Unlike tree-sitter which supports many languages via grammar files,
91
+ # Prism only parses Ruby. This class exists for API compatibility with
92
+ # other tree_haver backends.
93
+ #
94
+ # @example
95
+ # language = TreeHaver::Backends::Prism::Language.ruby
96
+ # parser.language = language
97
+ class Language < TreeHaver::Base::Language
98
+ # @param name [Symbol] language name (should be :ruby)
99
+ # @param options [Hash] Prism parsing options (e.g., frozen_string_literal, version)
100
+ def initialize(name = :ruby, options: {})
101
+ super(name, backend: :prism, options: options)
102
+
103
+ return if self.name == :ruby
104
+
105
+ raise TreeHaver::NotAvailable,
106
+ 'Prism only supports Ruby parsing. ' \
107
+ "Got language: #{name.inspect}"
108
+ end
109
+
110
+ # Compare languages for equality by options (since name is always :ruby)
111
+ #
112
+ # @param other [Object] object to compare with
113
+ # @return [Integer, nil] -1, 0, 1, or nil if not comparable
114
+ def <=>(other)
115
+ return unless other.is_a?(TreeHaver::Base::Language)
116
+ return unless other.backend == backend
117
+
118
+ options.to_a.sort <=> other.options.to_a.sort
119
+ end
120
+
121
+ class << self
122
+ # Create a Ruby language instance (convenience method)
123
+ #
124
+ # @param options [Hash] Prism parsing options
125
+ # @option options [Boolean] :frozen_string_literal frozen string literal pragma
126
+ # @option options [String] :version Ruby version to parse as (e.g., "3.3.0")
127
+ # @option options [Symbol] :command_line command line option (-e, -n, etc.)
128
+ # @return [Language]
129
+ # @example
130
+ # lang = TreeHaver::Backends::Prism::Language.ruby
131
+ # lang = TreeHaver::Backends::Prism::Language.ruby(frozen_string_literal: true)
132
+ def ruby(options = {})
133
+ new(:ruby, options: options)
134
+ end
135
+
136
+ # Load language from library path (API compatibility)
137
+ #
138
+ # Prism only supports Ruby, so path and symbol parameters are ignored.
139
+ #
140
+ # @param _path [String] Ignored - Prism doesn't load external grammars
141
+ # @param symbol [String, nil] Ignored - Prism only supports Ruby
142
+ # @param name [String, nil] Language name hint (defaults to :ruby)
143
+ # @return [Language] Ruby language
144
+ # @raise [TreeHaver::NotAvailable] if requested language is not Ruby
145
+ def from_library(_path = nil, symbol: nil, name: nil)
146
+ lang_name = name || :ruby
147
+
148
+ unless lang_name == :ruby
149
+ raise TreeHaver::NotAvailable,
150
+ "Prism backend only supports Ruby, not #{lang_name}. " \
151
+ "Use a tree-sitter backend for #{lang_name} support."
152
+ end
153
+
154
+ ruby
155
+ end
156
+
157
+ alias from_path from_library
158
+ end
159
+ end
160
+
161
+ # Prism parser wrapper
162
+ #
163
+ # Wraps Prism to provide a tree-sitter-like API for parsing Ruby code.
164
+ class Parser < TreeHaver::Base::Parser
165
+ # Create a new Prism parser instance
166
+ #
167
+ # @raise [TreeHaver::NotAvailable] if prism is not available
168
+ def initialize
169
+ super
170
+ raise TreeHaver::NotAvailable, 'prism not available' unless TreeHaver::Backends::Prism.available?
171
+
172
+ @options = {}
173
+ end
174
+
175
+ # Set the language for this parser
176
+ #
177
+ # Note: TreeHaver::Parser unwraps language objects before calling this method.
178
+ # This backend receives the Language wrapper (since Prism::Language stores options).
179
+ #
180
+ # @param lang [Language, Symbol] Prism language (should be :ruby or Language instance)
181
+ # @return [void]
182
+ def language=(lang)
183
+ case lang
184
+ when Language
185
+ @language = lang
186
+ @options = lang.options
187
+ when Symbol, String
188
+ if lang.to_sym == :ruby
189
+ @language = Language.ruby
190
+ @options = {}
191
+ else
192
+ raise ArgumentError,
193
+ "Prism only supports Ruby parsing. Got: #{lang.inspect}"
194
+ end
195
+ else
196
+ raise ArgumentError,
197
+ "Expected Prism::Language or :ruby, got #{lang.class}"
198
+ end
199
+ end
200
+
201
+ # Parse source code
202
+ #
203
+ # @param source [String] the Ruby source code to parse
204
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
205
+ # @raise [TreeHaver::NotAvailable] if no language is set
206
+ def parse(source)
207
+ raise TreeHaver::NotAvailable, 'No language loaded (use parser.language = :ruby)' unless @language
208
+
209
+ # Use Prism.parse with options
210
+ prism_result = ::Prism.parse(source, **@options)
211
+ Tree.new(prism_result, source)
212
+ end
213
+
214
+ # Parse source code (compatibility with tree-sitter API)
215
+ #
216
+ # Prism doesn't support incremental parsing, so old_tree is ignored.
217
+ #
218
+ # @param old_tree [TreeHaver::Tree, nil] ignored (no incremental parsing support)
219
+ # @param source [String] the Ruby source code to parse
220
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
221
+ def parse_string(old_tree, source) # rubocop:disable Lint/UnusedMethodArgument
222
+ parse(source) # Prism doesn't support incremental parsing
223
+ end
224
+ end
225
+
226
+ # Prism tree wrapper
227
+ #
228
+ # Wraps a Prism::ParseResult to provide tree-sitter-compatible API.
229
+ #
230
+ # @api private
231
+ class Tree < TreeHaver::Base::Tree
232
+ # @return [::Prism::ParseResult] the underlying Prism parse result
233
+ attr_reader :parse_result
234
+
235
+ def initialize(parse_result, source)
236
+ super(parse_result, source: source)
237
+ @parse_result = parse_result
238
+ end
239
+
240
+ # Get the root node of the parse tree
241
+ #
242
+ # @return [Node] wrapped root node
243
+ def root_node
244
+ Node.new(@parse_result.value, source)
245
+ end
246
+
247
+ # Check if the parse had errors
248
+ #
249
+ # @return [Boolean]
250
+ def has_errors?
251
+ @parse_result.failure?
252
+ end
253
+
254
+ # Get parse errors
255
+ #
256
+ # @return [Array<::Prism::ParseError>]
257
+ def errors
258
+ @parse_result.errors
259
+ end
260
+
261
+ # Get parse warnings
262
+ #
263
+ # @return [Array<::Prism::ParseWarning>]
264
+ def warnings
265
+ @parse_result.warnings
266
+ end
267
+
268
+ # Get comments from the parse
269
+ #
270
+ # @return [Array<Comment>]
271
+ def comments
272
+ @comments ||= begin
273
+ hint_map = comment_hint_map
274
+ @parse_result.comments.map do |comment|
275
+ Comment.new(comment, source: source, attachment_hint: hint_map[comment.object_id])
276
+ end
277
+ end
278
+ end
279
+
280
+ # Get magic comments (e.g., frozen_string_literal)
281
+ #
282
+ # @return [Array<::Prism::MagicComment>]
283
+ def magic_comments
284
+ @parse_result.magic_comments
285
+ end
286
+
287
+ # Get data locations (__END__ section)
288
+ #
289
+ # @return [::Prism::Location, nil]
290
+ def data_loc
291
+ @parse_result.data_loc
292
+ end
293
+
294
+ private
295
+
296
+ def comment_hint_map
297
+ comments = @parse_result.comments
298
+ lines = source&.lines || []
299
+
300
+ comments.each_with_object({}) do |comment, hints|
301
+ hints[comment.object_id] = classify_comment_hint(comment, lines)
302
+ end
303
+ end
304
+
305
+ def classify_comment_hint(comment, lines)
306
+ line = lines[comment.location.start_line - 1].to_s
307
+ prefix = line[0...comment.location.start_column]
308
+ return :inline if prefix.match?(/\S/)
309
+
310
+ remaining_nonempty = Array(lines[comment.location.end_line..]).reject { |candidate| candidate.strip.empty? }
311
+ return :trailing if remaining_nonempty.empty? || remaining_nonempty.all? do |candidate|
312
+ candidate.lstrip.start_with?('#')
313
+ end
314
+
315
+ :leading
316
+ end
317
+ end
318
+
319
+ # Prism comment wrapper.
320
+ #
321
+ # Prism exposes native Ruby comments via Prism::Comment subclasses and
322
+ # Prism::Location objects. This wrapper normalizes those objects onto a
323
+ # small parser-facing contract for downstream consumers.
324
+ class Comment < TreeHaver::Base::Comment
325
+ def location
326
+ inner_comment.location
327
+ end
328
+
329
+ def type
330
+ self.class.comment_type_for(inner_comment.class)
331
+ end
332
+
333
+ def text
334
+ inner_comment.slice
335
+ end
336
+
337
+ def start_byte
338
+ location.start_offset
339
+ end
340
+
341
+ def end_byte
342
+ location.end_offset
343
+ end
344
+
345
+ def start_point
346
+ {
347
+ row: location.start_line - 1,
348
+ column: location.start_column
349
+ }
350
+ end
351
+
352
+ def end_point
353
+ {
354
+ row: location.end_line - 1,
355
+ column: location.end_column
356
+ }
357
+ end
358
+
359
+ def style
360
+ :line
361
+ end
362
+
363
+ def opening_delimiter
364
+ '#'
365
+ end
366
+
367
+ class << self
368
+ def comment_type_for(klass)
369
+ klass.name.split('::').last
370
+ .gsub(/([a-z\d])([A-Z])/, '\\1_\\2')
371
+ .downcase
372
+ end
373
+ end
374
+ end
375
+
376
+ # Prism node wrapper
377
+ #
378
+ # Wraps Prism::Node objects to provide tree-sitter-compatible node API.
379
+ #
380
+ # Prism nodes provide:
381
+ # - type: class name without "Node" suffix (e.g., ProgramNode → "program")
382
+ # - location: ::Prism::Location with start/end offsets and line/column
383
+ # - child_nodes: array of child nodes
384
+ # - Various node-specific accessors
385
+ #
386
+ # @api private
387
+ class Node < TreeHaver::Base::Node
388
+ def initialize(node, source)
389
+ super(node, source: source)
390
+ end
391
+
392
+ # Get node type from Prism class name
393
+ #
394
+ # Converts PrismClassName to tree-sitter-style type string.
395
+ # Example: CallNode → "call_node", ProgramNode → "program_node"
396
+ #
397
+ # @return [String] node type in snake_case
398
+ def type
399
+ return 'nil' if inner_node.nil?
400
+
401
+ # Convert class name to snake_case type
402
+ class_name = inner_node.class.name.split('::').last
403
+ class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
404
+ end
405
+
406
+ # Alias for type (API compatibility)
407
+ # @return [String] node type
408
+ def kind
409
+ type
410
+ end
411
+
412
+ # Get byte offset where the node starts
413
+ #
414
+ # @return [Integer]
415
+ def start_byte
416
+ return 0 if inner_node.nil? || !inner_node.respond_to?(:location)
417
+
418
+ loc = inner_node.location
419
+ loc&.start_offset || 0
420
+ end
421
+
422
+ # Get byte offset where the node ends
423
+ #
424
+ # @return [Integer]
425
+ def end_byte
426
+ return 0 if inner_node.nil? || !inner_node.respond_to?(:location)
427
+
428
+ loc = inner_node.location
429
+ loc&.end_offset || 0
430
+ end
431
+
432
+ # Get the start position as row/column (0-based)
433
+ #
434
+ # @return [Hash{Symbol => Integer}]
435
+ def start_point
436
+ return { row: 0, column: 0 } if inner_node.nil? || !inner_node.respond_to?(:location)
437
+
438
+ loc = inner_node.location
439
+ return { row: 0, column: 0 } unless loc
440
+
441
+ { row: (loc.start_line - 1), column: loc.start_column }
442
+ end
443
+
444
+ # Get the end position as row/column (0-based)
445
+ #
446
+ # @return [Hash{Symbol => Integer}]
447
+ def end_point
448
+ return { row: 0, column: 0 } if inner_node.nil? || !inner_node.respond_to?(:location)
449
+
450
+ loc = inner_node.location
451
+ return { row: 0, column: 0 } unless loc
452
+
453
+ { row: (loc.end_line - 1), column: loc.end_column }
454
+ end
455
+
456
+ # Get all child nodes
457
+ #
458
+ # @return [Array<Node>] array of wrapped child nodes
459
+ def children
460
+ return [] if inner_node.nil?
461
+ return [] unless inner_node.respond_to?(:child_nodes)
462
+
463
+ inner_node.child_nodes.compact.map { |n| Node.new(n, source) }
464
+ end
465
+
466
+ # Get the text content of this node
467
+ #
468
+ # @return [String]
469
+ def text
470
+ return '' if inner_node.nil?
471
+
472
+ if inner_node.respond_to?(:slice)
473
+ inner_node.slice
474
+ else
475
+ super
476
+ end
477
+ end
478
+
479
+ # Alias for Prism compatibility
480
+ alias slice text
481
+
482
+ # Check if this node has errors
483
+ #
484
+ # @return [Boolean]
485
+ def has_error?
486
+ return false if inner_node.nil?
487
+
488
+ # Check if this is an error node type
489
+ return true if type.include?('missing') || type.include?('error')
490
+
491
+ # Check children recursively (Prism error nodes are usually children)
492
+ return false unless inner_node.respond_to?(:child_nodes)
493
+
494
+ inner_node.child_nodes.compact.any? { |n| n.class.name.to_s.include?('Missing') }
495
+ end
496
+
497
+ # Check if this node is a "missing" node (error recovery)
498
+ #
499
+ # @return [Boolean]
500
+ def missing?
501
+ return false if inner_node.nil?
502
+
503
+ type.include?('missing')
504
+ end
505
+
506
+ # Get a child by field name (Prism node accessor)
507
+ #
508
+ # Prism nodes have specific accessors for their children.
509
+ #
510
+ # @param name [String, Symbol] field/accessor name
511
+ # @return [Node, nil] wrapped child node
512
+ def child_by_field_name(name)
513
+ return if inner_node.nil?
514
+ return unless inner_node.respond_to?(name)
515
+
516
+ result = inner_node.public_send(name)
517
+ return if result.nil?
518
+
519
+ # Wrap if it's a node
520
+ result.is_a?(::Prism::Node) ? Node.new(result, source) : nil
521
+ end
522
+
523
+ alias field child_by_field_name
524
+
525
+ # String representation
526
+ #
527
+ # @return [String]
528
+ def to_s
529
+ text
530
+ end
531
+
532
+ # Check if node responds to a method (includes delegation to inner_node)
533
+ #
534
+ # @param method_name [Symbol] method to check
535
+ # @param include_private [Boolean] include private methods
536
+ # @return [Boolean]
537
+ def respond_to_missing?(method_name, include_private = false)
538
+ return false if inner_node.nil?
539
+
540
+ inner_node.respond_to?(method_name, include_private) || super
541
+ end
542
+
543
+ # Delegate unknown methods to the underlying Prism node
544
+ #
545
+ # This provides passthrough access for Prism-specific node methods
546
+ # like `receiver`, `message`, `arguments`, etc.
547
+ #
548
+ # @param method_name [Symbol] method to call
549
+ # @param args [Array] arguments to pass
550
+ # @param kwargs [Hash] keyword arguments
551
+ # @param block [Proc] block to pass
552
+ # @return [Object] result from the underlying node
553
+ def method_missing(method_name, *args, **kwargs, &block)
554
+ if inner_node&.respond_to?(method_name)
555
+ inner_node.public_send(method_name, *args, **kwargs, &block)
556
+ else
557
+ super
558
+ end
559
+ end
560
+ end
561
+
562
+ # Register the availability checker for RSpec dependency tags
563
+ TreeHaver::BackendRegistry.register_availability_checker(:prism) do
564
+ available?
565
+ end
566
+ end
567
+ end
568
+ end