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,565 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
5
+ # Parslet backend using pure Ruby PEG parser
6
+ #
7
+ # This backend wraps Parslet-based parsers (like the toml gem) to provide a
8
+ # pure Ruby alternative to tree-sitter. Parslet is a PEG (Parsing Expression
9
+ # Grammar) parser generator written in Ruby that produces Hash/Array/Slice
10
+ # results rather than a traditional AST.
11
+ #
12
+ # Unlike tree-sitter backends which are language-agnostic runtime parsers,
13
+ # Parslet parsers are grammar-specific and defined as Ruby classes. Each
14
+ # language needs its own Parslet grammar (e.g., TOML::Parslet for TOML).
15
+ #
16
+ # @note This backend requires a Parslet grammar class for the specific language
17
+ # @see https://github.com/kschiess/parslet Parslet parser generator
18
+ # @see https://github.com/jm/toml toml gem (TOML Parslet grammar)
19
+ #
20
+ # @example Using with toml gem
21
+ # require "toml"
22
+ #
23
+ # parser = TreeHaver::Parser.new
24
+ # # For Parslet, "language" is actually a grammar class
25
+ # parser.language = TOML::Parslet
26
+ # tree = parser.parse(toml_source)
27
+ module Parslet
28
+ @load_attempted = false
29
+ @loaded = false
30
+
31
+ # Check if the Parslet backend is available
32
+ #
33
+ # Attempts to require parslet on first call and caches the result.
34
+ #
35
+ # @return [Boolean] true if parslet gem is available
36
+ # @example
37
+ # if TreeHaver::Backends::Parslet.available?
38
+ # puts "Parslet backend is ready"
39
+ # end
40
+ class << self
41
+ def available?
42
+ return @loaded if @load_attempted
43
+
44
+ @load_attempted = true
45
+ begin
46
+ require 'parslet'
47
+ @loaded = true
48
+ rescue LoadError
49
+ @loaded = false
50
+ rescue StandardError
51
+ @loaded = false
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::Parslet.capabilities
70
+ # # => { backend: :parslet, query: false, bytes_field: true, incremental: false, comment_support: :none }
71
+ def capabilities
72
+ return {} unless available?
73
+
74
+ {
75
+ backend: :parslet,
76
+ query: false, # Parslet doesn't have a query API like tree-sitter
77
+ bytes_field: true, # Parslet::Slice provides offset and length
78
+ incremental: false, # Parslet doesn't support incremental parsing
79
+ pure_ruby: true, # Parslet is pure Ruby (portable)
80
+ comment_support: :none
81
+ }
82
+ end
83
+ end
84
+
85
+ # Parslet grammar wrapper
86
+ #
87
+ # Unlike tree-sitter which loads compiled .so files, Parslet uses Ruby classes
88
+ # that define grammars. This class wraps a Parslet grammar class.
89
+ #
90
+ # @example
91
+ # # For TOML, use toml gem's grammar
92
+ # language = TreeHaver::Backends::Parslet::Language.new(TOML::Parslet)
93
+ class Language
94
+ include Comparable
95
+
96
+ # The Parslet grammar class
97
+ # @return [Class] Parslet grammar class (e.g., TOML::Parslet)
98
+ attr_reader :grammar_class
99
+
100
+ # The backend this language is for
101
+ # @return [Symbol]
102
+ attr_reader :backend
103
+
104
+ # @param grammar_class [Class] A Parslet grammar class (inherits from ::Parslet::Parser)
105
+ def initialize(grammar_class)
106
+ unless valid_grammar_class?(grammar_class)
107
+ raise TreeHaver::NotAvailable,
108
+ 'Grammar class must be a Parslet::Parser subclass or respond to :new and return a parser with :parse. ' \
109
+ 'Expected a Parslet grammar class (e.g., TOML::Parslet).'
110
+ end
111
+ @grammar_class = grammar_class
112
+ @backend = :parslet
113
+ end
114
+
115
+ # Get the language name
116
+ #
117
+ # Derives a name from the grammar class name.
118
+ #
119
+ # @return [Symbol] language name
120
+ def language_name
121
+ # Derive name from grammar class (e.g., TOML::Parslet -> :toml)
122
+ return :unknown unless @grammar_class.respond_to?(:name) && @grammar_class.name
123
+
124
+ name = @grammar_class.name.to_s.split('::').first.downcase
125
+ name.to_sym
126
+ end
127
+
128
+ # Alias for language_name (API compatibility)
129
+ alias name language_name
130
+
131
+ # Compare languages for equality
132
+ #
133
+ # Parslet languages are equal if they have the same backend and grammar_class.
134
+ # Grammar class uniquely identifies a Parslet language.
135
+ #
136
+ # @param other [Object] object to compare with
137
+ # @return [Integer, nil] -1, 0, 1, or nil if not comparable
138
+ def <=>(other)
139
+ return unless other.is_a?(Language)
140
+ return unless other.backend == @backend
141
+
142
+ # Compare by grammar_class name (classes are compared by object_id by default)
143
+ @grammar_class.name <=> other.grammar_class.name
144
+ end
145
+
146
+ # Hash value for this language (for use in Sets/Hashes)
147
+ # @return [Integer]
148
+ def hash
149
+ [@backend, @grammar_class.name].hash
150
+ end
151
+
152
+ # Alias eql? to ==
153
+ alias eql? ==
154
+
155
+ # Load language from library path (API compatibility)
156
+ #
157
+ # Parslet grammars are Ruby classes, not shared libraries. This method
158
+ # provides API compatibility with tree-sitter backends by looking up
159
+ # registered Parslet grammars by name.
160
+ #
161
+ # For full API consistency, register a Parslet grammar with:
162
+ # TreeHaver.register_language(:toml, grammar_class: TOML::Parslet)
163
+ #
164
+ # Then this method will find it when called via `TreeHaver.parser_for(:toml)`.
165
+ #
166
+ # @param path [String, nil] Ignored for Parslet (used to derive language name)
167
+ # @param symbol [String, nil] Used to derive language name if path not provided
168
+ # @param name [String, Symbol, nil] Language name to look up
169
+ # @return [Language] Parslet language wrapper
170
+ # @raise [TreeHaver::NotAvailable] if no Parslet grammar is registered for the language
171
+ class << self
172
+ def from_library(path = nil, symbol: nil, name: nil)
173
+ # Derive language name from path, symbol, or explicit name
174
+ lang_name = name&.to_sym ||
175
+ symbol&.to_s&.sub(/^tree_sitter_/, '')&.to_sym ||
176
+ path && TreeHaver::LibraryPathUtils.derive_language_name_from_path(path)&.to_sym
177
+
178
+ unless lang_name
179
+ raise TreeHaver::NotAvailable,
180
+ 'Parslet backend requires a language name. ' \
181
+ 'Provide name: parameter or register a grammar with TreeHaver.register_language.'
182
+ end
183
+
184
+ # Look up registered Parslet grammar
185
+ registration = TreeHaver::LanguageRegistry.registered(lang_name, :parslet)
186
+
187
+ unless registration
188
+ raise TreeHaver::NotAvailable,
189
+ "No Parslet grammar registered for #{lang_name.inspect}. " \
190
+ "Register one with: TreeHaver.register_language(:#{lang_name}, grammar_class: YourGrammar)"
191
+ end
192
+
193
+ grammar_class = registration[:grammar_class]
194
+ new(grammar_class)
195
+ end
196
+
197
+ alias from_path from_library
198
+ end
199
+
200
+ private
201
+
202
+ def valid_grammar_class?(klass)
203
+ return false unless klass.respond_to?(:new)
204
+
205
+ # Check if it's a Parslet::Parser subclass
206
+ return true if defined?(::Parslet::Parser) && (klass < ::Parslet::Parser)
207
+
208
+ # Fallback: check if it can create an instance that responds to parse
209
+ begin
210
+ instance = klass.new
211
+ instance.respond_to?(:parse)
212
+ rescue StandardError
213
+ false
214
+ end
215
+ end
216
+ end
217
+
218
+ # Parslet parser wrapper
219
+ #
220
+ # Wraps Parslet grammar classes to provide a tree-sitter-like API.
221
+ class Parser
222
+ # Create a new Parslet parser instance
223
+ #
224
+ # @raise [TreeHaver::NotAvailable] if parslet gem is not available
225
+ def initialize
226
+ raise TreeHaver::NotAvailable, 'parslet gem not available' unless Parslet.available?
227
+
228
+ @grammar = nil
229
+ @backend = :parslet
230
+ end
231
+
232
+ attr_reader :backend
233
+
234
+ # Set the grammar for this parser
235
+ #
236
+ # Accepts either a Parslet::Language wrapper or a raw Parslet grammar class.
237
+ # When passed a Language wrapper, extracts the grammar_class from it.
238
+ # When passed a raw grammar class, uses it directly.
239
+ #
240
+ # This flexibility allows both patterns:
241
+ # parser.language = TreeHaver::Backends::Parslet::Language.new(TOML::Parslet)
242
+ # parser.language = TOML::Parslet # Also works
243
+ #
244
+ # @param grammar [Language, Class] Parslet Language wrapper or grammar class
245
+ # @return [void]
246
+ def language=(grammar)
247
+ # Accept Language wrapper or raw grammar class
248
+ actual_grammar = case grammar
249
+ when Language
250
+ grammar.grammar_class
251
+ else
252
+ grammar
253
+ end
254
+
255
+ unless actual_grammar.respond_to?(:new)
256
+ raise ArgumentError,
257
+ 'Expected Parslet grammar class with new method or Language wrapper, ' \
258
+ "got #{grammar.class}"
259
+ end
260
+ @grammar = actual_grammar
261
+ end
262
+
263
+ # Parse source code
264
+ #
265
+ # @param source [String] the source code to parse
266
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
267
+ # @raise [TreeHaver::NotAvailable] if no grammar is set
268
+ # @raise [::Parslet::ParseFailed] if parsing fails
269
+ def parse(source)
270
+ raise TreeHaver::NotAvailable, 'No grammar loaded' unless @grammar
271
+
272
+ begin
273
+ parser_instance = @grammar.new
274
+ parslet_result = parser_instance.parse(source)
275
+ # Return raw Parslet result wrapped in Tree - TreeHaver::Parser will wrap it
276
+ Tree.new(parslet_result, source)
277
+ rescue ::Parslet::ParseFailed => e
278
+ # Re-raise with more context
279
+ raise TreeHaver::Error, "Parse error: #{e.message}"
280
+ end
281
+ end
282
+
283
+ # Parse source code (compatibility with tree-sitter API)
284
+ #
285
+ # Parslet doesn't support incremental parsing, so old_tree is ignored.
286
+ #
287
+ # @param old_tree [TreeHaver::Tree, nil] ignored (no incremental parsing support)
288
+ # @param source [String] the source code to parse
289
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
290
+ def parse_string(old_tree, source) # rubocop:disable Lint/UnusedMethodArgument
291
+ parse(source) # Parslet doesn't support incremental parsing
292
+ end
293
+ end
294
+
295
+ # Parslet tree wrapper
296
+ #
297
+ # Wraps Parslet parse results (Hash/Array/Slice) to provide
298
+ # tree-sitter-compatible API.
299
+ #
300
+ # Inherits from Base::Tree to get shared methods like #errors, #warnings,
301
+ # #comments, #has_error?, and #inspect.
302
+ #
303
+ # @api private
304
+ class Tree < TreeHaver::Base::Tree
305
+ # The raw Parslet parse result
306
+ # @return [Hash, Array, Parslet::Slice] The parse result
307
+ attr_reader :parslet_result
308
+
309
+ def initialize(parslet_result, source)
310
+ @parslet_result = parslet_result
311
+ super(parslet_result, source: source)
312
+ end
313
+
314
+ def root_node
315
+ Node.new(@parslet_result, @source, type: 'document')
316
+ end
317
+ end
318
+
319
+ # Parslet node wrapper
320
+ #
321
+ # Wraps Parslet parse results (Hash/Array/Slice) to provide tree-sitter-compatible node API.
322
+ #
323
+ # Parslet produces different result types:
324
+ # - Hash: Named captures like {:key => value, :value => ...}
325
+ # - Array: Repeated captures like [{...}, {...}]
326
+ # - Parslet::Slice: Terminal string values with position info
327
+ # - String: Plain strings (less common)
328
+ #
329
+ # This wrapper normalizes these into a tree-sitter-like node structure.
330
+ #
331
+ # Inherits from Base::Node to get shared methods like #first_child, #last_child,
332
+ # #to_s, #inspect, #==, #<=>, #source_position, #start_line, #end_line, etc.
333
+ #
334
+ # @api private
335
+ class Node < TreeHaver::Base::Node
336
+ attr_reader :value, :node_type
337
+
338
+ def initialize(value, source, type: nil, key: nil)
339
+ @value = value
340
+ @node_type = type || infer_type(key)
341
+ @key = key
342
+ super(value, source: source)
343
+ end
344
+
345
+ # -- Required API Methods (from Base::Node) ----------------------------
346
+
347
+ # Get node type
348
+ #
349
+ # For Parslet results:
350
+ # - Hash keys become node types for their values
351
+ # - Arrays become "sequence" type
352
+ # - Slices use their parent's key as type
353
+ #
354
+ # @return [String] the node type
355
+ def type
356
+ @node_type
357
+ end
358
+
359
+ # Get position information from Parslet::Slice if available
360
+ #
361
+ # @return [Integer] byte offset where this node starts
362
+ def start_byte
363
+ case @value
364
+ when ::Parslet::Slice
365
+ @value.offset
366
+ when Hash
367
+ # Find first slice in hash values
368
+ first_slice = find_first_slice(@value)
369
+ first_slice&.offset || 0
370
+ when Array
371
+ # Find first slice in array
372
+ first_slice = find_first_slice(@value)
373
+ first_slice&.offset || 0
374
+ else
375
+ 0
376
+ end
377
+ end
378
+
379
+ # @return [Integer] byte offset where this node ends
380
+ def end_byte
381
+ case @value
382
+ when ::Parslet::Slice
383
+ @value.offset + @value.size
384
+ when Hash
385
+ # Find last slice in hash values
386
+ last_slice = find_last_slice(@value)
387
+ last_slice ? (last_slice.offset + last_slice.size) : @source.length
388
+ when Array
389
+ # Find last slice in array
390
+ last_slice = find_last_slice(@value)
391
+ last_slice ? (last_slice.offset + last_slice.size) : @source.length
392
+ else
393
+ @source.length
394
+ end
395
+ end
396
+
397
+ # Get all children
398
+ #
399
+ # @return [Array<Node>] child nodes
400
+ def children
401
+ case @value
402
+ when Hash
403
+ @value.map { |k, v| Node.new(v, @source, key: k) }
404
+ when Array
405
+ @value.map.with_index { |v, i| Node.new(v, @source, type: "element_#{i}") }
406
+ else
407
+ []
408
+ end
409
+ end
410
+
411
+ # -- Overridden Methods ------------------------------------------------
412
+
413
+ # Override start_point to calculate from source
414
+ # @return [Hash{Symbol => Integer}] {row: 0, column: 0}
415
+ def start_point
416
+ calculate_point(start_byte)
417
+ end
418
+
419
+ # Override end_point to calculate from source
420
+ # @return [Hash{Symbol => Integer}] {row: 0, column: 0}
421
+ def end_point
422
+ calculate_point(end_byte)
423
+ end
424
+
425
+ # Override text to handle Parslet-specific value types
426
+ # @return [String] matched text
427
+ def text
428
+ case @value
429
+ when ::Parslet::Slice
430
+ @value.to_s
431
+ when String
432
+ @value
433
+ when Hash, Array
434
+ @source[start_byte...end_byte] || ''
435
+ else
436
+ @value.to_s
437
+ end
438
+ end
439
+
440
+ # Override child to handle negative indices properly
441
+ # @param index [Integer] child index
442
+ # @return [Node, nil] child node or nil
443
+ def child(index)
444
+ return if index.negative?
445
+
446
+ case @value
447
+ when Hash
448
+ keys = @value.keys
449
+ return if index >= keys.size
450
+
451
+ key = keys[index]
452
+ Node.new(@value[key], @source, key: key)
453
+ when Array
454
+ return if index >= @value.size
455
+
456
+ Node.new(@value[index], @source, type: 'element')
457
+ end
458
+ end
459
+
460
+ # Override child_count for efficiency (avoid building full children array)
461
+ # @return [Integer] child count
462
+ def child_count
463
+ case @value
464
+ when Hash
465
+ @value.keys.size
466
+ when Array
467
+ @value.size
468
+ else
469
+ 0
470
+ end
471
+ end
472
+
473
+ # Check if node is named
474
+ #
475
+ # Hash keys in Parslet results are "named" in tree-sitter terminology.
476
+ #
477
+ # @return [Boolean] true if this node has a key
478
+ def named?
479
+ !@key.nil? || @value.is_a?(Hash)
480
+ end
481
+
482
+ # Check if this node represents a structural element vs a terminal/token
483
+ #
484
+ # @return [Boolean] true if this is a structural (non-terminal) node
485
+ def structural?
486
+ @value.is_a?(Hash) || @value.is_a?(Array)
487
+ end
488
+
489
+ private
490
+
491
+ def calculate_point(offset)
492
+ return { row: 0, column: 0 } if offset <= 0
493
+
494
+ lines_before = @source[0...offset].count("\n")
495
+ line_start = (@source.rindex("\n", offset - 1) if offset > 0)
496
+ line_start ||= -1
497
+ column = offset - line_start - 1
498
+ { row: lines_before, column: column }
499
+ end
500
+
501
+ def infer_type(key)
502
+ return key.to_s if key
503
+
504
+ case @value
505
+ when ::Parslet::Slice
506
+ 'slice'
507
+ when Hash
508
+ 'hash'
509
+ when Array
510
+ 'array'
511
+ when String
512
+ 'string'
513
+ else
514
+ 'unknown'
515
+ end
516
+ end
517
+
518
+ # Find the first Parslet::Slice in a nested structure
519
+ def find_first_slice(obj)
520
+ case obj
521
+ when ::Parslet::Slice
522
+ obj
523
+ when Hash
524
+ obj.values.each do |v|
525
+ result = find_first_slice(v)
526
+ return result if result
527
+ end
528
+ nil
529
+ when Array
530
+ obj.each do |v|
531
+ result = find_first_slice(v)
532
+ return result if result
533
+ end
534
+ nil
535
+ end
536
+ end
537
+
538
+ # Find the last Parslet::Slice in a nested structure
539
+ def find_last_slice(obj)
540
+ case obj
541
+ when ::Parslet::Slice
542
+ obj
543
+ when Hash
544
+ obj.values.reverse_each do |v|
545
+ result = find_last_slice(v)
546
+ return result if result
547
+ end
548
+ nil
549
+ when Array
550
+ obj.reverse_each do |v|
551
+ result = find_last_slice(v)
552
+ return result if result
553
+ end
554
+ nil
555
+ end
556
+ end
557
+ end
558
+
559
+ # Register the availability checker for RSpec dependency tags
560
+ TreeHaver::BackendRegistry.register_availability_checker(:parslet) do
561
+ available?
562
+ end
563
+ end
564
+ end
565
+ end