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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +299 -660
- data/lib/tree_haver/backend_api.rb +78 -35
- data/lib/tree_haver/backend_context.rb +28 -0
- data/lib/tree_haver/backend_registry.rb +119 -382
- data/lib/tree_haver/backends/citrus.rb +55 -53
- data/lib/tree_haver/backends/ffi.rb +105 -101
- data/lib/tree_haver/backends/java.rb +92 -76
- data/lib/tree_haver/backends/mri.rb +44 -39
- data/lib/tree_haver/backends/parslet.rb +53 -48
- data/lib/tree_haver/backends/prism.rb +140 -43
- data/lib/tree_haver/backends/psych.rb +30 -26
- data/lib/tree_haver/backends/rust.rb +21 -17
- data/lib/tree_haver/backends/tslp.rb +274 -0
- data/lib/tree_haver/base/comment.rb +320 -0
- data/lib/tree_haver/base/language.rb +2 -2
- data/lib/tree_haver/base/node.rb +39 -31
- data/lib/tree_haver/base/parser.rb +4 -0
- data/lib/tree_haver/base/point.rb +3 -3
- data/lib/tree_haver/base/tree.rb +1 -1
- data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
- data/lib/tree_haver/contracts.rb +1025 -0
- data/lib/tree_haver/grammar_finder.rb +125 -70
- data/lib/tree_haver/kaitai_backend.rb +30 -0
- data/lib/tree_haver/language.rb +36 -37
- data/lib/tree_haver/language_pack.rb +41 -0
- data/lib/tree_haver/language_registry.rb +34 -3
- data/lib/tree_haver/library_path_utils.rb +7 -7
- data/lib/tree_haver/node.rb +40 -31
- data/lib/tree_haver/parser.rb +44 -37
- data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
- data/lib/tree_haver/path_validator.rb +39 -36
- data/lib/tree_haver/peg_backends.rb +76 -0
- data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
- data/lib/tree_haver/rspec.rb +1 -31
- data/lib/tree_haver/tree.rb +16 -7
- data/lib/tree_haver/version.rb +5 -14
- data/lib/tree_haver.rb +536 -1240
- data/sig/tree_haver.rbs +1 -229
- data.tar.gz.sig +0 -0
- metadata +154 -70
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -1393
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -359
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/lib/tree_haver/base.rb +0 -12
- data/lib/tree_haver/compat.rb +0 -43
- data/lib/tree_haver/rspec/testable_node.rb +0 -217
- data/sig/tree_haver/backends.rbs +0 -352
- data/sig/tree_haver/grammar_finder.rbs +0 -29
- 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
|
|
42
|
-
|
|
41
|
+
return @loaded if @load_attempted
|
|
42
|
+
|
|
43
|
+
@load_attempted = true
|
|
43
44
|
begin
|
|
44
|
-
require
|
|
45
|
-
@loaded = true
|
|
45
|
+
require 'citrus'
|
|
46
|
+
@loaded = true
|
|
46
47
|
rescue LoadError
|
|
47
|
-
@loaded = false
|
|
48
|
-
# :
|
|
48
|
+
@loaded = false
|
|
49
|
+
# simplecov:disable defensive code - StandardError during require is extremely rare
|
|
49
50
|
rescue StandardError
|
|
50
|
-
@loaded = false
|
|
51
|
-
# :
|
|
51
|
+
@loaded = false
|
|
52
|
+
# simplecov:enable
|
|
52
53
|
end
|
|
53
|
-
@loaded
|
|
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
|
|
62
|
-
@loaded = false
|
|
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
|
-
|
|
107
|
-
|
|
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(
|
|
123
|
-
name.sub(/rb$/,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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,
|
|
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)
|
|
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
|
|
338
|
-
return
|
|
339
|
-
return
|
|
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
|
|
461
|
+
return 'repeat'
|
|
458
462
|
elsif /^\(.*\)\?$/.match?(str)
|
|
459
|
-
return
|
|
463
|
+
return 'optional'
|
|
460
464
|
elsif /^.*\|.*$/.match?(str)
|
|
461
|
-
return
|
|
465
|
+
return 'choice'
|
|
462
466
|
end
|
|
463
467
|
|
|
464
|
-
|
|
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
|
|