tree_haver 7.0.0 → 7.1.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.
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 +490 -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 +8 -8
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +56 -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,490 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
5
+ # Citrus backend using pure Ruby PEG parser
6
+ #
7
+ # This backend wraps Citrus-based parsers (like toml-rb) to provide a
8
+ # pure Ruby alternative to tree-sitter. Citrus is a PEG (Parsing Expression
9
+ # Grammar) parser generator written in Ruby.
10
+ #
11
+ # Unlike tree-sitter backends which are language-agnostic runtime parsers,
12
+ # Citrus parsers are grammar-specific and compiled into Ruby code. Each
13
+ # language needs its own Citrus grammar (e.g., toml-rb for TOML).
14
+ #
15
+ # @note This backend requires a Citrus grammar for the specific language
16
+ # @see https://github.com/mjackson/citrus Citrus parser generator
17
+ # @see https://github.com/emancu/toml-rb toml-rb (TOML Citrus grammar)
18
+ #
19
+ # @example Using with toml-rb
20
+ # require "toml-rb"
21
+ #
22
+ # parser = TreeHaver::Parser.new
23
+ # # For Citrus, "language" is actually a grammar module
24
+ # parser.language = TomlRB::Document
25
+ # tree = parser.parse(toml_source)
26
+ module Citrus
27
+ @load_attempted = false
28
+ @loaded = false
29
+
30
+ # Check if the Citrus backend is available
31
+ #
32
+ # Attempts to require citrus on first call and caches the result.
33
+ #
34
+ # @return [Boolean] true if citrus gem is available
35
+ # @example
36
+ # if TreeHaver::Backends::Citrus.available?
37
+ # puts "Citrus backend is ready"
38
+ # end
39
+ class << self
40
+ def available?
41
+ return @loaded if @load_attempted
42
+
43
+ @load_attempted = true
44
+ begin
45
+ require 'citrus'
46
+ @loaded = true
47
+ rescue LoadError
48
+ @loaded = false
49
+ # simplecov:disable defensive code - StandardError during require is extremely rare
50
+ rescue StandardError
51
+ @loaded = false
52
+ # simplecov:enable
53
+ end
54
+ @loaded
55
+ end
56
+
57
+ # Reset the load state (primarily for testing)
58
+ #
59
+ # @return [void]
60
+ # @api private
61
+ def reset!
62
+ @load_attempted = false
63
+ @loaded = false
64
+ end
65
+
66
+ # Get capabilities supported by this backend
67
+ #
68
+ # @return [Hash{Symbol => Object}] capability map
69
+ # @example
70
+ # TreeHaver::Backends::Citrus.capabilities
71
+ # # => { backend: :citrus, query: false, bytes_field: true, incremental: false, comment_support: :none }
72
+ def capabilities
73
+ return {} unless available?
74
+
75
+ {
76
+ backend: :citrus,
77
+ query: false, # Citrus doesn't have a query API like tree-sitter
78
+ bytes_field: true, # Citrus::Match provides offset and length
79
+ incremental: false, # Citrus doesn't support incremental parsing
80
+ pure_ruby: true, # Citrus is pure Ruby (portable)
81
+ comment_support: :none
82
+ }
83
+ end
84
+ end
85
+
86
+ # Citrus grammar wrapper
87
+ #
88
+ # Unlike tree-sitter which loads compiled .so files, Citrus uses Ruby modules
89
+ # that define grammars. This class wraps a Citrus grammar module.
90
+ #
91
+ # @example
92
+ # # For TOML, use toml-rb's grammar
93
+ # language = TreeHaver::Backends::Citrus::Language.new(TomlRB::Document)
94
+ class Language
95
+ include Comparable
96
+
97
+ # The Citrus grammar module
98
+ # @return [Module] Citrus grammar module (e.g., TomlRB::Document)
99
+ attr_reader :grammar_module
100
+
101
+ # The backend this language is for
102
+ # @return [Symbol]
103
+ attr_reader :backend
104
+
105
+ # @param grammar_module [Module] A Citrus grammar module with a parse method
106
+ def initialize(grammar_module)
107
+ unless grammar_module.respond_to?(:parse)
108
+ raise TreeHaver::NotAvailable,
109
+ 'Grammar module must respond to :parse. ' \
110
+ 'Expected a Citrus grammar module (e.g., TomlRB::Document).'
111
+ end
112
+ @grammar_module = grammar_module
113
+ @backend = :citrus
114
+ end
115
+
116
+ # Get the language name
117
+ #
118
+ # Derives a name from the grammar module name.
119
+ #
120
+ # @return [Symbol] language name
121
+ def language_name
122
+ # Derive name from grammar module (e.g., TomlRB::Document -> :toml)
123
+ return :unknown unless @grammar_module.respond_to?(:name) && @grammar_module.name
124
+
125
+ name = @grammar_module.name.to_s.split('::').first.downcase
126
+ name.sub(/rb$/, '').to_sym
127
+ end
128
+
129
+ # Alias for language_name (API compatibility)
130
+ alias name language_name
131
+
132
+ # Compare languages for equality
133
+ #
134
+ # Citrus languages are equal if they have the same backend and grammar_module.
135
+ # Grammar module uniquely identifies a Citrus language.
136
+ #
137
+ # @param other [Object] object to compare with
138
+ # @return [Integer, nil] -1, 0, 1, or nil if not comparable
139
+ def <=>(other)
140
+ return unless other.is_a?(Language)
141
+ return unless other.backend == @backend
142
+
143
+ # Compare by grammar_module name (modules are compared by object_id by default)
144
+ @grammar_module.name <=> other.grammar_module.name
145
+ end
146
+
147
+ # Hash value for this language (for use in Sets/Hashes)
148
+ # @return [Integer]
149
+ def hash
150
+ [@backend, @grammar_module.name].hash
151
+ end
152
+
153
+ # Alias eql? to ==
154
+ alias eql? ==
155
+
156
+ # Load language from library path (API compatibility)
157
+ #
158
+ # Citrus grammars are Ruby modules, not shared libraries. This method
159
+ # provides API compatibility with tree-sitter backends by looking up
160
+ # registered Citrus grammars by name.
161
+ #
162
+ # For full API consistency, register a Citrus grammar with:
163
+ # TreeHaver.register_language(:toml, grammar_module: TomlRB::Document)
164
+ #
165
+ # Then this method will find it when called via `TreeHaver.parser_for(:toml)`.
166
+ #
167
+ # @param path [String, nil] Ignored for Citrus (used to derive language name)
168
+ # @param symbol [String, nil] Used to derive language name if path not provided
169
+ # @param name [String, Symbol, nil] Language name to look up
170
+ # @return [Language] Citrus language wrapper
171
+ # @raise [TreeHaver::NotAvailable] if no Citrus grammar is registered for the language
172
+ class << self
173
+ def from_library(path = nil, symbol: nil, name: nil)
174
+ # Derive language name from path, symbol, or explicit name
175
+ lang_name = name&.to_sym ||
176
+ symbol&.to_s&.sub(/^tree_sitter_/, '')&.to_sym ||
177
+ path && TreeHaver::LibraryPathUtils.derive_language_name_from_path(path)&.to_sym
178
+
179
+ unless lang_name
180
+ raise TreeHaver::NotAvailable,
181
+ 'Citrus backend requires a language name. ' \
182
+ 'Provide name: parameter or register a grammar with TreeHaver.register_language.'
183
+ end
184
+
185
+ # Look up registered Citrus grammar
186
+ registration = TreeHaver::LanguageRegistry.registered(lang_name, :citrus)
187
+
188
+ unless registration
189
+ raise TreeHaver::NotAvailable,
190
+ "No Citrus grammar registered for #{lang_name.inspect}. " \
191
+ "Register one with: TreeHaver.register_language(:#{lang_name}, grammar_module: YourGrammar)"
192
+ end
193
+
194
+ grammar_module = registration[:grammar_module]
195
+ new(grammar_module)
196
+ end
197
+
198
+ alias from_path from_library
199
+ end
200
+ end
201
+
202
+ # Citrus parser wrapper
203
+ #
204
+ # Wraps Citrus grammar modules to provide a tree-sitter-like API.
205
+ class Parser
206
+ # Create a new Citrus parser instance
207
+ #
208
+ # @raise [TreeHaver::NotAvailable] if citrus gem is not available
209
+ def initialize
210
+ raise TreeHaver::NotAvailable, 'citrus gem not available' unless Citrus.available?
211
+
212
+ @grammar = nil
213
+ @backend = :citrus
214
+ end
215
+
216
+ attr_reader :backend
217
+
218
+ # Set the grammar for this parser
219
+ #
220
+ # Accepts either a Citrus::Language wrapper or a raw Citrus grammar module.
221
+ # When passed a Language wrapper, extracts the grammar_module from it.
222
+ # When passed a raw grammar module, uses it directly.
223
+ #
224
+ # This flexibility allows both patterns:
225
+ # parser.language = TreeHaver::Backends::Citrus::Language.new(TomlRB::Document)
226
+ # parser.language = TomlRB::Document # Also works
227
+ #
228
+ # @param grammar [Language, Module] Citrus Language wrapper or grammar module
229
+ # @return [void]
230
+ def language=(grammar)
231
+ # Accept Language wrapper or raw grammar module
232
+ actual_grammar = case grammar
233
+ when Language
234
+ grammar.grammar_module
235
+ else
236
+ grammar
237
+ end
238
+
239
+ unless actual_grammar.respond_to?(:parse)
240
+ raise ArgumentError,
241
+ 'Expected Citrus grammar module with parse method or Language wrapper, ' \
242
+ "got #{grammar.class}"
243
+ end
244
+ @grammar = actual_grammar
245
+ end
246
+
247
+ # Parse source code
248
+ #
249
+ # @param source [String] the source code to parse
250
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
251
+ # @raise [TreeHaver::NotAvailable] if no grammar is set
252
+ # @raise [::Citrus::ParseError] if parsing fails
253
+ def parse(source)
254
+ raise TreeHaver::NotAvailable, 'No grammar loaded' unless @grammar
255
+
256
+ begin
257
+ citrus_match = @grammar.parse(source)
258
+ # Return raw Citrus::Tree - TreeHaver::Parser will wrap it
259
+ Tree.new(citrus_match, source)
260
+ rescue ::Citrus::ParseError => e
261
+ # Re-raise with more context
262
+ raise TreeHaver::Error, "Parse error: #{e.message}"
263
+ end
264
+ end
265
+
266
+ # Parse source code (compatibility with tree-sitter API)
267
+ #
268
+ # Citrus doesn't support incremental parsing, so old_tree is ignored.
269
+ #
270
+ # @param old_tree [TreeHaver::Tree, nil] ignored (no incremental parsing support)
271
+ # @param source [String] the source code to parse
272
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
273
+ def parse_string(old_tree, source) # rubocop:disable Lint/UnusedMethodArgument
274
+ parse(source) # Citrus doesn't support incremental parsing
275
+ end
276
+ end
277
+
278
+ # Citrus tree wrapper
279
+ #
280
+ # Wraps a Citrus::Match (which represents the parse tree) to provide
281
+ # tree-sitter-compatible API.
282
+ #
283
+ # Inherits from Base::Tree to get shared methods like #errors, #warnings,
284
+ # #comments, #has_error?, and #inspect.
285
+ #
286
+ # @api private
287
+ class Tree < TreeHaver::Base::Tree
288
+ # The raw Citrus::Match root
289
+ # @return [Citrus::Match] The root match
290
+ attr_reader :root_match
291
+
292
+ def initialize(root_match, source)
293
+ @root_match = root_match
294
+ super(root_match, source: source)
295
+ end
296
+
297
+ def root_node
298
+ Node.new(@root_match, @source)
299
+ end
300
+ end
301
+
302
+ # Citrus node wrapper
303
+ #
304
+ # Wraps Citrus::Match objects to provide tree-sitter-compatible node API.
305
+ #
306
+ # Citrus::Match provides:
307
+ # - events[0]: rule name (Symbol) - used as type
308
+ # - offset: byte position
309
+ # - length: byte length
310
+ # - string: matched text
311
+ # - matches: child matches
312
+ # - captures: named groups
313
+ #
314
+ # Inherits from Base::Node to get shared methods like #first_child, #last_child,
315
+ # #to_s, #inspect, #==, #<=>, #source_position, #start_line, #end_line, etc.
316
+ #
317
+ # Language-specific helpers can be mixed in for convenience:
318
+ # require "tree_haver/backends/citrus/toml_helpers"
319
+ # TreeHaver::Backends::Citrus::Node.include(TreeHaver::Backends::Citrus::TomlHelpers)
320
+ #
321
+ # @api private
322
+ class Node < TreeHaver::Base::Node
323
+ attr_reader :match
324
+
325
+ def initialize(match, source)
326
+ @match = match
327
+ super(match, source: source)
328
+ end
329
+
330
+ # -- Required API Methods (from Base::Node) ----------------------------
331
+
332
+ # Get node type from Citrus rule name
333
+ #
334
+ # Uses Citrus grammar introspection to dynamically determine node types.
335
+ # Works with any Citrus grammar without language-specific knowledge.
336
+ #
337
+ # Strategy:
338
+ # 1. Check if first event has a .name method (returns Symbol) - use that
339
+ # 2. If first event is a Symbol directly - use that
340
+ # 3. For compound rules (Repeat, Choice), recurse into first match
341
+ #
342
+ # @return [String] rule name from grammar
343
+ def type
344
+ return 'unknown' unless @match.respond_to?(:events)
345
+ return 'unknown' unless @match.events.is_a?(Array)
346
+ return 'unknown' if @match.events.empty?
347
+
348
+ extract_type_from_event(@match.events.first)
349
+ end
350
+
351
+ def start_byte
352
+ @match.offset
353
+ end
354
+
355
+ def end_byte
356
+ @match.offset + @match.length
357
+ end
358
+
359
+ def children
360
+ return [] unless @match.respond_to?(:matches)
361
+
362
+ @match.matches.map { |m| Node.new(m, @source) }
363
+ end
364
+
365
+ # -- Overridden Methods ------------------------------------------------
366
+
367
+ # Override start_point to calculate from source
368
+ def start_point
369
+ calculate_point(@match.offset)
370
+ end
371
+
372
+ # Override end_point to calculate from source
373
+ def end_point
374
+ calculate_point(@match.offset + @match.length)
375
+ end
376
+
377
+ # Override text to use Citrus match string
378
+ def text
379
+ @match.string
380
+ end
381
+
382
+ # Override child_count for efficiency (avoid building full children array)
383
+ def child_count
384
+ @match.respond_to?(:matches) ? @match.matches.size : 0
385
+ end
386
+
387
+ # Override child to handle negative indices properly
388
+ def child(index)
389
+ return if index.negative?
390
+ return unless @match.respond_to?(:matches)
391
+ return if index >= @match.matches.size
392
+
393
+ Node.new(@match.matches[index], @source)
394
+ end
395
+
396
+ # Check if this node represents a structural element vs a terminal/token
397
+ #
398
+ # Uses Citrus grammar's terminal? method to determine if this is
399
+ # a structural rule (like "table", "keyvalue") vs a terminal token
400
+ # (like "[", "=", whitespace).
401
+ #
402
+ # @return [Boolean] true if this is a structural (non-terminal) node
403
+ def structural?
404
+ return false unless @match.respond_to?(:events)
405
+ return false if @match.events.empty?
406
+
407
+ first_event = @match.events.first
408
+
409
+ # Check if event has terminal? method (Citrus rule object)
410
+ return !first_event.terminal? if first_event.respond_to?(:terminal?)
411
+
412
+ # For Symbol events, try to look up in grammar
413
+ if first_event.is_a?(Symbol) && @match.respond_to?(:grammar)
414
+ grammar = @match.grammar
415
+ if grammar.respond_to?(:rules) && grammar.rules.key?(first_event)
416
+ rule = grammar.rules[first_event]
417
+ return !rule.terminal? if rule.respond_to?(:terminal?)
418
+ end
419
+ end
420
+
421
+ # Default: assume structural if not a simple string/regex terminal
422
+ true
423
+ end
424
+
425
+ private
426
+
427
+ # Extract type name from a Citrus event object
428
+ #
429
+ # Handles different event types:
430
+ # - Objects with .name method (Citrus rule objects) -> use .name
431
+ # - Symbol -> use directly
432
+ # - Compound rules (Repeat, Choice) -> check string representation
433
+ #
434
+ # @param event [Object] Citrus event object
435
+ # @return [String] type name
436
+ def extract_type_from_event(event)
437
+ # Case 1: Event has .name method (returns Symbol)
438
+ if event.respond_to?(:name)
439
+ name = event.name
440
+ return name.to_s if name.is_a?(Symbol)
441
+ end
442
+
443
+ # Case 2: Event is a Symbol directly (most common for child nodes)
444
+ return event.to_s if event.is_a?(Symbol)
445
+
446
+ # Case 3: Event is a String
447
+ return event if event.is_a?(String)
448
+
449
+ # Case 4: For compound rules (Repeat, Choice), try string parsing first
450
+ # This avoids recursion issues
451
+ str = event.to_s
452
+
453
+ # Try to extract rule name from string representation
454
+ # Examples: "table", "(comment | table)*", "space?", etc.
455
+ return ::Regexp.last_match(1) if str =~ /^([a-z_][a-z0-9_]*)/i
456
+
457
+ # If we have a pattern like "(rule1 | rule2)*", we can't determine
458
+ # the type without looking at actual matches, but that causes recursion
459
+ # So just return a generic type based on the pattern
460
+ case str
461
+ when /^\(.*\)\*$/
462
+ return 'repeat'
463
+ when /^\(.*\)\?$/
464
+ return 'optional'
465
+ when /^.*\|.*$/
466
+ return 'choice'
467
+ end
468
+
469
+ 'unknown'
470
+ end
471
+
472
+ def calculate_point(offset)
473
+ return { row: 0, column: 0 } if offset <= 0
474
+
475
+ lines_before = @source[0...offset].count("\n")
476
+ # Find the newline before this offset (or -1 if we're on line 0)
477
+ line_start = (@source.rindex("\n", offset - 1) if offset.positive?)
478
+ line_start ||= -1
479
+ column = offset - line_start - 1
480
+ { row: lines_before, column: column }
481
+ end
482
+ end
483
+
484
+ # Register the availability checker for RSpec dependency tags
485
+ TreeHaver::BackendRegistry.register_availability_checker(:citrus) do
486
+ available?
487
+ end
488
+ end
489
+ end
490
+ end