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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +1959 -0
- data/lib/tree_haver/backend_api.rb +392 -0
- data/lib/tree_haver/backend_registry.rb +153 -3
- data/lib/tree_haver/backends/citrus.rb +489 -0
- data/lib/tree_haver/backends/ffi.rb +1013 -0
- data/lib/tree_haver/backends/java.rb +909 -0
- data/lib/tree_haver/backends/mri.rb +367 -0
- data/lib/tree_haver/backends/parslet.rb +565 -0
- data/lib/tree_haver/backends/prism.rb +568 -0
- data/lib/tree_haver/backends/psych.rb +379 -0
- data/lib/tree_haver/backends/rust.rb +243 -0
- 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 +98 -0
- data/lib/tree_haver/base/node.rb +330 -0
- data/lib/tree_haver/base/parser.rb +28 -0
- data/lib/tree_haver/base/point.rb +48 -0
- data/lib/tree_haver/base/tree.rb +128 -0
- data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
- data/lib/tree_haver/contracts.rb +661 -96
- data/lib/tree_haver/grammar_finder.rb +429 -0
- data/lib/tree_haver/kaitai_backend.rb +2 -2
- data/lib/tree_haver/language.rb +294 -0
- data/lib/tree_haver/language_pack.rb +17 -166
- data/lib/tree_haver/language_registry.rb +221 -0
- data/lib/tree_haver/library_path_utils.rb +80 -0
- data/lib/tree_haver/node.rb +588 -0
- data/lib/tree_haver/parser.rb +445 -0
- data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
- data/lib/tree_haver/path_validator.rb +356 -0
- data/lib/tree_haver/peg_backends.rb +7 -7
- data/lib/tree_haver/point.rb +27 -0
- data/lib/tree_haver/rspec/dependency_tags.rb +52 -0
- data/lib/tree_haver/rspec.rb +3 -0
- data/lib/tree_haver/tree.rb +267 -0
- data/lib/tree_haver/version.rb +5 -3
- data/lib/tree_haver.rb +613 -8
- data/sig/tree_haver.rbs +6 -0
- data.tar.gz.sig +0 -0
- metadata +314 -13
- 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
|