tree_haver 5.0.5 → 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 (59) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +299 -660
  5. data/lib/tree_haver/backend_api.rb +78 -35
  6. data/lib/tree_haver/backend_context.rb +28 -0
  7. data/lib/tree_haver/backend_registry.rb +119 -382
  8. data/lib/tree_haver/backends/citrus.rb +55 -53
  9. data/lib/tree_haver/backends/ffi.rb +105 -101
  10. data/lib/tree_haver/backends/java.rb +92 -76
  11. data/lib/tree_haver/backends/mri.rb +44 -39
  12. data/lib/tree_haver/backends/parslet.rb +53 -48
  13. data/lib/tree_haver/backends/prism.rb +140 -43
  14. data/lib/tree_haver/backends/psych.rb +30 -26
  15. data/lib/tree_haver/backends/rust.rb +21 -17
  16. data/lib/tree_haver/backends/tslp.rb +274 -0
  17. data/lib/tree_haver/base/comment.rb +320 -0
  18. data/lib/tree_haver/base/language.rb +2 -2
  19. data/lib/tree_haver/base/node.rb +39 -31
  20. data/lib/tree_haver/base/parser.rb +4 -0
  21. data/lib/tree_haver/base/point.rb +3 -3
  22. data/lib/tree_haver/base/tree.rb +1 -1
  23. data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
  24. data/lib/tree_haver/contracts.rb +1025 -0
  25. data/lib/tree_haver/grammar_finder.rb +125 -70
  26. data/lib/tree_haver/kaitai_backend.rb +30 -0
  27. data/lib/tree_haver/language.rb +36 -37
  28. data/lib/tree_haver/language_pack.rb +41 -0
  29. data/lib/tree_haver/language_registry.rb +34 -3
  30. data/lib/tree_haver/library_path_utils.rb +7 -7
  31. data/lib/tree_haver/node.rb +40 -31
  32. data/lib/tree_haver/parser.rb +44 -37
  33. data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
  34. data/lib/tree_haver/path_validator.rb +39 -36
  35. data/lib/tree_haver/peg_backends.rb +76 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
  37. data/lib/tree_haver/rspec.rb +1 -31
  38. data/lib/tree_haver/tree.rb +16 -7
  39. data/lib/tree_haver/version.rb +5 -14
  40. data/lib/tree_haver.rb +536 -1240
  41. data/sig/tree_haver.rbs +1 -229
  42. data.tar.gz.sig +0 -0
  43. metadata +154 -70
  44. metadata.gz.sig +0 -0
  45. data/CHANGELOG.md +0 -1393
  46. data/CITATION.cff +0 -20
  47. data/CODE_OF_CONDUCT.md +0 -134
  48. data/CONTRIBUTING.md +0 -359
  49. data/FUNDING.md +0 -74
  50. data/LICENSE.txt +0 -21
  51. data/REEK +0 -0
  52. data/RUBOCOP.md +0 -71
  53. data/SECURITY.md +0 -21
  54. data/lib/tree_haver/base.rb +0 -12
  55. data/lib/tree_haver/compat.rb +0 -43
  56. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  57. data/sig/tree_haver/backends.rbs +0 -352
  58. data/sig/tree_haver/grammar_finder.rbs +0 -29
  59. data/sig/tree_haver/path_validator.rbs +0 -32
@@ -38,19 +38,20 @@ module TreeHaver
38
38
  # end
39
39
  class << self
40
40
  def available?
41
- return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
42
- @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
41
+ return @loaded if @load_attempted
42
+
43
+ @load_attempted = true
43
44
  begin
44
- require "citrus"
45
- @loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
45
+ require 'citrus'
46
+ @loaded = true
46
47
  rescue LoadError
47
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
48
- # :nocov: defensive code - StandardError during require is extremely rare
48
+ @loaded = false
49
+ # simplecov:disable defensive code - StandardError during require is extremely rare
49
50
  rescue StandardError
50
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
51
- # :nocov:
51
+ @loaded = false
52
+ # simplecov:enable
52
53
  end
53
- @loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
54
+ @loaded
54
55
  end
55
56
 
56
57
  # Reset the load state (primarily for testing)
@@ -58,8 +59,8 @@ module TreeHaver
58
59
  # @return [void]
59
60
  # @api private
60
61
  def reset!
61
- @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
62
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
62
+ @load_attempted = false
63
+ @loaded = false
63
64
  end
64
65
 
65
66
  # Get capabilities supported by this backend
@@ -67,15 +68,17 @@ module TreeHaver
67
68
  # @return [Hash{Symbol => Object}] capability map
68
69
  # @example
69
70
  # TreeHaver::Backends::Citrus.capabilities
70
- # # => { backend: :citrus, query: false, bytes_field: true, incremental: false }
71
+ # # => { backend: :citrus, query: false, bytes_field: true, incremental: false, comment_support: :none }
71
72
  def capabilities
72
73
  return {} unless available?
74
+
73
75
  {
74
76
  backend: :citrus,
75
77
  query: false, # Citrus doesn't have a query API like tree-sitter
76
78
  bytes_field: true, # Citrus::Match provides offset and length
77
79
  incremental: false, # Citrus doesn't support incremental parsing
78
80
  pure_ruby: true, # Citrus is pure Ruby (portable)
81
+ comment_support: :none
79
82
  }
80
83
  end
81
84
  end
@@ -103,8 +106,8 @@ module TreeHaver
103
106
  def initialize(grammar_module)
104
107
  unless grammar_module.respond_to?(:parse)
105
108
  raise TreeHaver::NotAvailable,
106
- "Grammar module must respond to :parse. " \
107
- "Expected a Citrus grammar module (e.g., TomlRB::Document)."
109
+ 'Grammar module must respond to :parse. ' \
110
+ 'Expected a Citrus grammar module (e.g., TomlRB::Document).'
108
111
  end
109
112
  @grammar_module = grammar_module
110
113
  @backend = :citrus
@@ -119,12 +122,12 @@ module TreeHaver
119
122
  # Derive name from grammar module (e.g., TomlRB::Document -> :toml)
120
123
  return :unknown unless @grammar_module.respond_to?(:name) && @grammar_module.name
121
124
 
122
- name = @grammar_module.name.to_s.split("::").first.downcase
123
- name.sub(/rb$/, "").to_sym
125
+ name = @grammar_module.name.to_s.split('::').first.downcase
126
+ name.sub(/rb$/, '').to_sym
124
127
  end
125
128
 
126
129
  # Alias for language_name (API compatibility)
127
- alias_method :name, :language_name
130
+ alias name language_name
128
131
 
129
132
  # Compare languages for equality
130
133
  #
@@ -148,7 +151,7 @@ module TreeHaver
148
151
  end
149
152
 
150
153
  # Alias eql? to ==
151
- alias_method :eql?, :==
154
+ alias eql? ==
152
155
 
153
156
  # Load language from library path (API compatibility)
154
157
  #
@@ -170,13 +173,13 @@ module TreeHaver
170
173
  def from_library(path = nil, symbol: nil, name: nil)
171
174
  # Derive language name from path, symbol, or explicit name
172
175
  lang_name = name&.to_sym ||
173
- symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym ||
174
- path && TreeHaver::LibraryPathUtils.derive_language_name_from_path(path)&.to_sym
176
+ symbol&.to_s&.sub(/^tree_sitter_/, '')&.to_sym ||
177
+ path && TreeHaver::LibraryPathUtils.derive_language_name_from_path(path)&.to_sym
175
178
 
176
179
  unless lang_name
177
180
  raise TreeHaver::NotAvailable,
178
- "Citrus backend requires a language name. " \
179
- "Provide name: parameter or register a grammar with TreeHaver.register_language."
181
+ 'Citrus backend requires a language name. ' \
182
+ 'Provide name: parameter or register a grammar with TreeHaver.register_language.'
180
183
  end
181
184
 
182
185
  # Look up registered Citrus grammar
@@ -184,15 +187,15 @@ module TreeHaver
184
187
 
185
188
  unless registration
186
189
  raise TreeHaver::NotAvailable,
187
- "No Citrus grammar registered for #{lang_name.inspect}. " \
188
- "Register one with: TreeHaver.register_language(:#{lang_name}, grammar_module: YourGrammar)"
190
+ "No Citrus grammar registered for #{lang_name.inspect}. " \
191
+ "Register one with: TreeHaver.register_language(:#{lang_name}, grammar_module: YourGrammar)"
189
192
  end
190
193
 
191
194
  grammar_module = registration[:grammar_module]
192
195
  new(grammar_module)
193
196
  end
194
197
 
195
- alias_method :from_path, :from_library
198
+ alias from_path from_library
196
199
  end
197
200
  end
198
201
 
@@ -204,10 +207,14 @@ module TreeHaver
204
207
  #
205
208
  # @raise [TreeHaver::NotAvailable] if citrus gem is not available
206
209
  def initialize
207
- raise TreeHaver::NotAvailable, "citrus gem not available" unless Citrus.available?
210
+ raise TreeHaver::NotAvailable, 'citrus gem not available' unless Citrus.available?
211
+
208
212
  @grammar = nil
213
+ @backend = :citrus
209
214
  end
210
215
 
216
+ attr_reader :backend
217
+
211
218
  # Set the grammar for this parser
212
219
  #
213
220
  # Accepts either a Citrus::Language wrapper or a raw Citrus grammar module.
@@ -223,16 +230,16 @@ module TreeHaver
223
230
  def language=(grammar)
224
231
  # Accept Language wrapper or raw grammar module
225
232
  actual_grammar = case grammar
226
- when Language
227
- grammar.grammar_module
228
- else
229
- grammar
230
- end
233
+ when Language
234
+ grammar.grammar_module
235
+ else
236
+ grammar
237
+ end
231
238
 
232
239
  unless actual_grammar.respond_to?(:parse)
233
240
  raise ArgumentError,
234
- "Expected Citrus grammar module with parse method or Language wrapper, " \
235
- "got #{grammar.class}"
241
+ 'Expected Citrus grammar module with parse method or Language wrapper, ' \
242
+ "got #{grammar.class}"
236
243
  end
237
244
  @grammar = actual_grammar
238
245
  end
@@ -244,7 +251,7 @@ module TreeHaver
244
251
  # @raise [TreeHaver::NotAvailable] if no grammar is set
245
252
  # @raise [::Citrus::ParseError] if parsing fails
246
253
  def parse(source)
247
- raise TreeHaver::NotAvailable, "No grammar loaded" unless @grammar
254
+ raise TreeHaver::NotAvailable, 'No grammar loaded' unless @grammar
248
255
 
249
256
  begin
250
257
  citrus_match = @grammar.parse(source)
@@ -264,7 +271,7 @@ module TreeHaver
264
271
  # @param source [String] the source code to parse
265
272
  # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
266
273
  def parse_string(old_tree, source) # rubocop:disable Lint/UnusedMethodArgument
267
- parse(source) # Citrus doesn't support incremental parsing
274
+ parse(source) # Citrus doesn't support incremental parsing
268
275
  end
269
276
  end
270
277
 
@@ -334,9 +341,9 @@ module TreeHaver
334
341
  #
335
342
  # @return [String] rule name from grammar
336
343
  def type
337
- return "unknown" unless @match.respond_to?(:events)
338
- return "unknown" unless @match.events.is_a?(Array)
339
- return "unknown" if @match.events.empty?
344
+ return 'unknown' unless @match.respond_to?(:events)
345
+ return 'unknown' unless @match.events.is_a?(Array)
346
+ return 'unknown' if @match.events.empty?
340
347
 
341
348
  extract_type_from_event(@match.events.first)
342
349
  end
@@ -351,6 +358,7 @@ module TreeHaver
351
358
 
352
359
  def children
353
360
  return [] unless @match.respond_to?(:matches)
361
+
354
362
  @match.matches.map { |m| Node.new(m, @source) }
355
363
  end
356
364
 
@@ -399,9 +407,7 @@ module TreeHaver
399
407
  first_event = @match.events.first
400
408
 
401
409
  # Check if event has terminal? method (Citrus rule object)
402
- if first_event.respond_to?(:terminal?)
403
- return !first_event.terminal?
404
- end
410
+ return !first_event.terminal? if first_event.respond_to?(:terminal?)
405
411
 
406
412
  # For Symbol events, try to look up in grammar
407
413
  if first_event.is_a?(Symbol) && @match.respond_to?(:grammar)
@@ -446,35 +452,31 @@ module TreeHaver
446
452
 
447
453
  # Try to extract rule name from string representation
448
454
  # Examples: "table", "(comment | table)*", "space?", etc.
449
- if str =~ /^([a-z_][a-z0-9_]*)/i
450
- return $1
451
- end
455
+ return ::Regexp.last_match(1) if str =~ /^([a-z_][a-z0-9_]*)/i
452
456
 
453
457
  # If we have a pattern like "(rule1 | rule2)*", we can't determine
454
458
  # the type without looking at actual matches, but that causes recursion
455
459
  # So just return a generic type based on the pattern
456
460
  if /^\(.*\)\*$/.match?(str)
457
- return "repeat"
461
+ return 'repeat'
458
462
  elsif /^\(.*\)\?$/.match?(str)
459
- return "optional"
463
+ return 'optional'
460
464
  elsif /^.*\|.*$/.match?(str)
461
- return "choice"
465
+ return 'choice'
462
466
  end
463
467
 
464
- "unknown"
468
+ 'unknown'
465
469
  end
466
470
 
467
471
  def calculate_point(offset)
468
- return {row: 0, column: 0} if offset <= 0
472
+ return { row: 0, column: 0 } if offset <= 0
469
473
 
470
474
  lines_before = @source[0...offset].count("\n")
471
475
  # Find the newline before this offset (or -1 if we're on line 0)
472
- line_start = if offset > 0
473
- @source.rindex("\n", offset - 1)
474
- end
476
+ line_start = (@source.rindex("\n", offset - 1) if offset > 0)
475
477
  line_start ||= -1
476
478
  column = offset - line_start - 1
477
- {row: lines_before, column: column}
479
+ { row: lines_before, column: column }
478
480
  end
479
481
  end
480
482