tree_sitter 0.1.0-aarch64-linux
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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/Makefile +116 -0
- data/README.md +466 -0
- data/lib/tree_sitter/3.2/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.3/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.4/tree_sitter.so +0 -0
- data/lib/tree_sitter/4.0/tree_sitter.so +0 -0
- data/lib/tree_sitter/formatting.rb +236 -0
- data/lib/tree_sitter/inserter.rb +306 -0
- data/lib/tree_sitter/query_rewriter.rb +314 -0
- data/lib/tree_sitter/refactor.rb +214 -0
- data/lib/tree_sitter/rewriter.rb +155 -0
- data/lib/tree_sitter/transformer.rb +324 -0
- data/lib/tree_sitter/version.rb +5 -0
- data/lib/tree_sitter.rb +25 -0
- metadata +96 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TreeSitter
|
|
4
|
+
# Query-based bulk editing for syntax tree nodes.
|
|
5
|
+
# Finds all nodes matching a tree-sitter query and applies transformations.
|
|
6
|
+
#
|
|
7
|
+
# @example Rename all function calls matching a pattern
|
|
8
|
+
# QueryRewriter.new(source, tree, language)
|
|
9
|
+
# .query('(call_expression function: (identifier) @fn_name)')
|
|
10
|
+
# .where { |match| match.captures.any? { |c| c.node.text == "old_name" } }
|
|
11
|
+
# .replace("@fn_name") { |node| "new_name" }
|
|
12
|
+
# .rewrite
|
|
13
|
+
#
|
|
14
|
+
# @example Remove all comments
|
|
15
|
+
# QueryRewriter.new(source, tree, language)
|
|
16
|
+
# .query('(line_comment) @comment')
|
|
17
|
+
# .remove("@comment")
|
|
18
|
+
# .rewrite
|
|
19
|
+
#
|
|
20
|
+
class QueryRewriter
|
|
21
|
+
# Represents a pending operation on a capture
|
|
22
|
+
Operation = Struct.new(:type, :capture_name, :transformer, :before, :after, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
attr_reader :source, :tree, :language, :edits
|
|
25
|
+
|
|
26
|
+
# Initialize a new QueryRewriter
|
|
27
|
+
#
|
|
28
|
+
# @param source [String] The source code to rewrite
|
|
29
|
+
# @param tree [TreeSitter::Tree] The parsed syntax tree
|
|
30
|
+
# @param language [TreeSitter::Language, String] The language for queries
|
|
31
|
+
# @param parser [TreeSitter::Parser, nil] Optional parser for re-parsing
|
|
32
|
+
def initialize(source, tree, language = nil, parser: nil)
|
|
33
|
+
@source = source.dup.freeze
|
|
34
|
+
@tree = tree
|
|
35
|
+
@language = resolve_language(language)
|
|
36
|
+
@parser = parser
|
|
37
|
+
@query_pattern = nil
|
|
38
|
+
@predicates = []
|
|
39
|
+
@operations = []
|
|
40
|
+
@edits = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set the query pattern to match against
|
|
44
|
+
#
|
|
45
|
+
# @param pattern [String] Tree-sitter query pattern
|
|
46
|
+
# @return [self] For method chaining
|
|
47
|
+
def query(pattern)
|
|
48
|
+
@query_pattern = pattern
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Filter matches based on a predicate
|
|
53
|
+
#
|
|
54
|
+
# @yield [QueryMatch] Block that returns true for matches to keep
|
|
55
|
+
# @return [self] For method chaining
|
|
56
|
+
def where(&predicate)
|
|
57
|
+
@predicates << predicate
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Replace captured nodes with new content
|
|
62
|
+
#
|
|
63
|
+
# @param capture_name [String] The @capture to replace (e.g., "@fn_name" or "fn_name")
|
|
64
|
+
# @yield [Node] Block that returns the replacement text (receives the captured node)
|
|
65
|
+
# @yieldreturn [String] The replacement text
|
|
66
|
+
# @return [self] For method chaining
|
|
67
|
+
def replace(capture_name, &transformer)
|
|
68
|
+
@operations << Operation.new(
|
|
69
|
+
type: :replace,
|
|
70
|
+
capture_name: normalize_capture_name(capture_name),
|
|
71
|
+
transformer: transformer || proc { "" },
|
|
72
|
+
)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Remove captured nodes
|
|
77
|
+
#
|
|
78
|
+
# @param capture_name [String] The @capture to remove
|
|
79
|
+
# @return [self] For method chaining
|
|
80
|
+
def remove(capture_name)
|
|
81
|
+
@operations << Operation.new(
|
|
82
|
+
type: :remove,
|
|
83
|
+
capture_name: normalize_capture_name(capture_name),
|
|
84
|
+
)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Insert content before captured nodes
|
|
89
|
+
#
|
|
90
|
+
# @param capture_name [String] The @capture reference point
|
|
91
|
+
# @yield [Node] Block that returns the content to insert
|
|
92
|
+
# @yieldreturn [String] The content to insert
|
|
93
|
+
# @return [self] For method chaining
|
|
94
|
+
def insert_before(capture_name, content = nil, &content_generator)
|
|
95
|
+
generator = content_generator || proc { content.to_s }
|
|
96
|
+
@operations << Operation.new(
|
|
97
|
+
type: :insert_before,
|
|
98
|
+
capture_name: normalize_capture_name(capture_name),
|
|
99
|
+
transformer: generator,
|
|
100
|
+
)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Insert content after captured nodes
|
|
105
|
+
#
|
|
106
|
+
# @param capture_name [String] The @capture reference point
|
|
107
|
+
# @yield [Node] Block that returns the content to insert
|
|
108
|
+
# @yieldreturn [String] The content to insert
|
|
109
|
+
# @return [self] For method chaining
|
|
110
|
+
def insert_after(capture_name, content = nil, &content_generator)
|
|
111
|
+
generator = content_generator || proc { content.to_s }
|
|
112
|
+
@operations << Operation.new(
|
|
113
|
+
type: :insert_after,
|
|
114
|
+
capture_name: normalize_capture_name(capture_name),
|
|
115
|
+
transformer: generator,
|
|
116
|
+
)
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Wrap captured nodes with before/after content
|
|
121
|
+
#
|
|
122
|
+
# @param capture_name [String] The @capture to wrap
|
|
123
|
+
# @param before [String, nil] Content before (or use block)
|
|
124
|
+
# @param after [String, nil] Content after
|
|
125
|
+
# @yield [Node] Optional block that returns [before, after] tuple
|
|
126
|
+
# @return [self] For method chaining
|
|
127
|
+
def wrap(capture_name, before: nil, after: nil, &block)
|
|
128
|
+
@operations << if block
|
|
129
|
+
Operation.new(
|
|
130
|
+
type: :wrap_dynamic,
|
|
131
|
+
capture_name: normalize_capture_name(capture_name),
|
|
132
|
+
transformer: block,
|
|
133
|
+
)
|
|
134
|
+
else
|
|
135
|
+
Operation.new(
|
|
136
|
+
type: :wrap,
|
|
137
|
+
capture_name: normalize_capture_name(capture_name),
|
|
138
|
+
before: before.to_s,
|
|
139
|
+
after: after.to_s,
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Execute the query and collect all matches
|
|
146
|
+
#
|
|
147
|
+
# @return [Array<QueryMatch>] All matches found
|
|
148
|
+
def matches
|
|
149
|
+
return [] unless @query_pattern && @language
|
|
150
|
+
|
|
151
|
+
ts_query = TreeSitter::Query.new(@language, @query_pattern)
|
|
152
|
+
cursor = TreeSitter::QueryCursor.new
|
|
153
|
+
|
|
154
|
+
all_matches = cursor.matches(ts_query, @tree.root_node, @source)
|
|
155
|
+
|
|
156
|
+
# Apply filters
|
|
157
|
+
@predicates.reduce(all_matches) do |matches, predicate|
|
|
158
|
+
matches.select(&predicate)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Apply all accumulated edits
|
|
163
|
+
#
|
|
164
|
+
# @return [String] The rewritten source code
|
|
165
|
+
def rewrite
|
|
166
|
+
build_edits
|
|
167
|
+
apply_edits
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Apply edits and return both source and new tree
|
|
171
|
+
#
|
|
172
|
+
# @return [Array<String, Tree>] The new source and re-parsed tree
|
|
173
|
+
def rewrite_with_tree
|
|
174
|
+
new_source = rewrite
|
|
175
|
+
|
|
176
|
+
parser = @parser || create_parser_from_tree
|
|
177
|
+
raise "No parser available for re-parsing" unless parser
|
|
178
|
+
|
|
179
|
+
new_tree = parser.parse(new_source)
|
|
180
|
+
[new_source, new_tree]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Get a preview of all edits that would be applied
|
|
184
|
+
#
|
|
185
|
+
# @return [Array<Hash>] Array of edit descriptions
|
|
186
|
+
def preview_edits
|
|
187
|
+
build_edits
|
|
188
|
+
@edits.map do |edit|
|
|
189
|
+
{
|
|
190
|
+
start_byte: edit[:start_byte],
|
|
191
|
+
end_byte: edit[:end_byte],
|
|
192
|
+
original: @source[edit[:start_byte]...edit[:end_byte]],
|
|
193
|
+
replacement: edit[:replacement],
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def resolve_language(language)
|
|
201
|
+
case language
|
|
202
|
+
when TreeSitter::Language
|
|
203
|
+
language
|
|
204
|
+
when String
|
|
205
|
+
TreeSitter.language(language)
|
|
206
|
+
when nil
|
|
207
|
+
@tree&.language
|
|
208
|
+
else
|
|
209
|
+
raise ArgumentError, "Invalid language: #{language.class}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def normalize_capture_name(name)
|
|
214
|
+
name.to_s.delete_prefix("@")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def build_edits
|
|
218
|
+
@edits = []
|
|
219
|
+
found_matches = matches
|
|
220
|
+
|
|
221
|
+
found_matches.each do |match|
|
|
222
|
+
@operations.each do |operation|
|
|
223
|
+
# Find captures matching this operation
|
|
224
|
+
captures = match.captures.select { |c| c.name == operation.capture_name }
|
|
225
|
+
|
|
226
|
+
captures.each do |capture|
|
|
227
|
+
node = capture.node
|
|
228
|
+
range = node.range
|
|
229
|
+
|
|
230
|
+
case operation.type
|
|
231
|
+
when :replace
|
|
232
|
+
replacement = operation.transformer.call(node)
|
|
233
|
+
@edits << {
|
|
234
|
+
start_byte: range.start_byte,
|
|
235
|
+
end_byte: range.end_byte,
|
|
236
|
+
replacement: replacement.to_s,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
when :remove
|
|
240
|
+
@edits << {
|
|
241
|
+
start_byte: range.start_byte,
|
|
242
|
+
end_byte: range.end_byte,
|
|
243
|
+
replacement: "",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
when :insert_before
|
|
247
|
+
content = operation.transformer.call(node)
|
|
248
|
+
@edits << {
|
|
249
|
+
start_byte: range.start_byte,
|
|
250
|
+
end_byte: range.start_byte,
|
|
251
|
+
replacement: content.to_s,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
when :insert_after
|
|
255
|
+
content = operation.transformer.call(node)
|
|
256
|
+
@edits << {
|
|
257
|
+
start_byte: range.end_byte,
|
|
258
|
+
end_byte: range.end_byte,
|
|
259
|
+
replacement: content.to_s,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
when :wrap
|
|
263
|
+
@edits << {
|
|
264
|
+
start_byte: range.start_byte,
|
|
265
|
+
end_byte: range.start_byte,
|
|
266
|
+
replacement: operation.before,
|
|
267
|
+
}
|
|
268
|
+
@edits << {
|
|
269
|
+
start_byte: range.end_byte,
|
|
270
|
+
end_byte: range.end_byte,
|
|
271
|
+
replacement: operation.after,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
when :wrap_dynamic
|
|
275
|
+
before_text, after_text = operation.transformer.call(node)
|
|
276
|
+
@edits << {
|
|
277
|
+
start_byte: range.start_byte,
|
|
278
|
+
end_byte: range.start_byte,
|
|
279
|
+
replacement: before_text.to_s,
|
|
280
|
+
}
|
|
281
|
+
@edits << {
|
|
282
|
+
start_byte: range.end_byte,
|
|
283
|
+
end_byte: range.end_byte,
|
|
284
|
+
replacement: after_text.to_s,
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def apply_edits
|
|
293
|
+
# Sort by position descending to apply from end to start
|
|
294
|
+
sorted = @edits.sort_by { |e| [-e[:start_byte], -e[:end_byte]] }
|
|
295
|
+
|
|
296
|
+
result = @source.dup
|
|
297
|
+
sorted.each do |edit|
|
|
298
|
+
result[edit[:start_byte]...edit[:end_byte]] = edit[:replacement]
|
|
299
|
+
end
|
|
300
|
+
result
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def create_parser_from_tree
|
|
304
|
+
return unless @tree
|
|
305
|
+
|
|
306
|
+
parser = TreeSitter::Parser.new
|
|
307
|
+
lang = @tree.language
|
|
308
|
+
parser.language = lang.name if lang
|
|
309
|
+
parser
|
|
310
|
+
rescue StandardError
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "query_rewriter"
|
|
4
|
+
require_relative "transformer"
|
|
5
|
+
require_relative "inserter"
|
|
6
|
+
|
|
7
|
+
module TreeSitter
|
|
8
|
+
# High-level refactoring operations built on QueryRewriter and Transformer.
|
|
9
|
+
# These methods provide common code transformation patterns with a simple API.
|
|
10
|
+
#
|
|
11
|
+
# @example Rename a function throughout the code
|
|
12
|
+
# TreeSitter::Refactor.rename_symbol(source, tree, lang,
|
|
13
|
+
# from: "old_name", to: "new_name")
|
|
14
|
+
#
|
|
15
|
+
module Refactor
|
|
16
|
+
class << self
|
|
17
|
+
# Rename a symbol (function, variable, type) throughout the code
|
|
18
|
+
#
|
|
19
|
+
# @param source [String] Source code
|
|
20
|
+
# @param tree [Tree] Parsed syntax tree
|
|
21
|
+
# @param language [Language] Language for queries
|
|
22
|
+
# @param from [String] Original name
|
|
23
|
+
# @param to [String] New name
|
|
24
|
+
# @param kind [Symbol] Type of symbol (:identifier, :function, :type, :variable)
|
|
25
|
+
# @return [String] Modified source code
|
|
26
|
+
def rename_symbol(source, tree, language, from:, to:, kind: :identifier)
|
|
27
|
+
query_pattern = build_rename_query(kind)
|
|
28
|
+
|
|
29
|
+
QueryRewriter.new(source, tree, language)
|
|
30
|
+
.query(query_pattern)
|
|
31
|
+
.where { |m| match_has_text?(m, from) }
|
|
32
|
+
.replace("@name") { to }
|
|
33
|
+
.rewrite
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Rename a struct/class field and its usages
|
|
37
|
+
#
|
|
38
|
+
# @param source [String] Source code
|
|
39
|
+
# @param tree [Tree] Parsed syntax tree
|
|
40
|
+
# @param language [Language] Language for queries
|
|
41
|
+
# @param struct_name [String, nil] Name of struct/class (nil for all)
|
|
42
|
+
# @param from [String] Old field name
|
|
43
|
+
# @param to [String] New field name
|
|
44
|
+
# @return [String] Modified source code
|
|
45
|
+
def rename_field(source, tree, language, struct_name: nil, from:, to:)
|
|
46
|
+
# Query for field declarations and field accesses
|
|
47
|
+
query_pattern = <<~QUERY
|
|
48
|
+
[
|
|
49
|
+
(field_declaration name: (field_identifier) @name)
|
|
50
|
+
(field_expression field: (field_identifier) @name)
|
|
51
|
+
(field_identifier) @name
|
|
52
|
+
]
|
|
53
|
+
QUERY
|
|
54
|
+
|
|
55
|
+
QueryRewriter.new(source, tree, language)
|
|
56
|
+
.query(query_pattern)
|
|
57
|
+
.where { |m| match_has_text?(m, from) }
|
|
58
|
+
.replace("@name") { to }
|
|
59
|
+
.rewrite
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Extract code into a new function
|
|
63
|
+
#
|
|
64
|
+
# @param source [String] Source code
|
|
65
|
+
# @param tree [Tree] Parsed syntax tree
|
|
66
|
+
# @param language [Language] Language for queries
|
|
67
|
+
# @param node [Node] Node to extract
|
|
68
|
+
# @param name [String] Name for extracted function
|
|
69
|
+
# @param parameters [Array<String>] Parameter names
|
|
70
|
+
# @param insert_after [Node, nil] Where to insert the new function
|
|
71
|
+
# @return [String] Modified source code
|
|
72
|
+
def extract_function(source, tree, language, node:, name:, parameters: [], insert_after: nil)
|
|
73
|
+
# Build function call
|
|
74
|
+
param_list = parameters.join(", ")
|
|
75
|
+
call_reference = parameters.empty? ? "#{name}()" : "#{name}(#{param_list})"
|
|
76
|
+
|
|
77
|
+
# Build function definition
|
|
78
|
+
node_text = source[node.start_byte...node.end_byte]
|
|
79
|
+
param_decl = parameters.map { |p| "#{p}: _" }.join(", ")
|
|
80
|
+
fn_def = "fn #{name}(#{param_decl}) {\n #{node_text}\n}"
|
|
81
|
+
|
|
82
|
+
# Determine where to insert
|
|
83
|
+
insert_target = insert_after || find_enclosing_function(node) || tree.root_node
|
|
84
|
+
|
|
85
|
+
Transformer.new(source, tree)
|
|
86
|
+
.extract(node, to: insert_target, reference: call_reference) { fn_def }
|
|
87
|
+
.rewrite
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Inline a variable (replace usages with its value)
|
|
91
|
+
#
|
|
92
|
+
# @param source [String] Source code
|
|
93
|
+
# @param tree [Tree] Parsed syntax tree
|
|
94
|
+
# @param language [Language] Language for queries
|
|
95
|
+
# @param name [String] Variable name to inline
|
|
96
|
+
# @param scope [Node, nil] Scope to limit inlining (nil for entire tree)
|
|
97
|
+
# @return [String] Modified source code
|
|
98
|
+
def inline_variable(source, tree, language, name:, scope: nil)
|
|
99
|
+
# Find the variable declaration and its value
|
|
100
|
+
decl_query = "(let_declaration pattern: (identifier) @var_name value: (_) @value)"
|
|
101
|
+
|
|
102
|
+
query = TreeSitter::Query.new(language, decl_query)
|
|
103
|
+
cursor = TreeSitter::QueryCursor.new
|
|
104
|
+
root = scope || tree.root_node
|
|
105
|
+
matches = cursor.matches(query, root, source)
|
|
106
|
+
|
|
107
|
+
# Find the declaration for our variable
|
|
108
|
+
decl_match = matches.find do |m|
|
|
109
|
+
m.captures.any? { |c| c.name == "var_name" && c.node.text == name }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return source unless decl_match
|
|
113
|
+
|
|
114
|
+
# Get the value to inline
|
|
115
|
+
value_capture = decl_match.captures.find { |c| c.name == "value" }
|
|
116
|
+
return source unless value_capture
|
|
117
|
+
|
|
118
|
+
value_text = value_capture.node.text
|
|
119
|
+
|
|
120
|
+
# Find all usages and replace
|
|
121
|
+
usage_query = "(identifier) @usage"
|
|
122
|
+
|
|
123
|
+
QueryRewriter.new(source, tree, language)
|
|
124
|
+
.query(usage_query)
|
|
125
|
+
.where { |m| match_has_text?(m, name) && !declaration?(m, source) }
|
|
126
|
+
.replace("@usage") { value_text }
|
|
127
|
+
.rewrite
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Add an attribute/annotation to items matching a query
|
|
131
|
+
#
|
|
132
|
+
# @param source [String] Source code
|
|
133
|
+
# @param tree [Tree] Parsed syntax tree
|
|
134
|
+
# @param language [Language] Language for queries
|
|
135
|
+
# @param query_pattern [String] Query to match items
|
|
136
|
+
# @param attribute [String] Attribute to add (e.g., "#[derive(Debug)]")
|
|
137
|
+
# @return [String] Modified source code
|
|
138
|
+
def add_attribute(source, tree, language, query_pattern:, attribute:)
|
|
139
|
+
QueryRewriter.new(source, tree, language)
|
|
140
|
+
.query(query_pattern)
|
|
141
|
+
.insert_before("@item") { "#{attribute}\n" }
|
|
142
|
+
.rewrite
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Remove items matching a query
|
|
146
|
+
#
|
|
147
|
+
# @param source [String] Source code
|
|
148
|
+
# @param tree [Tree] Parsed syntax tree
|
|
149
|
+
# @param language [Language] Language for queries
|
|
150
|
+
# @param query_pattern [String] Query to match items to remove
|
|
151
|
+
# @param capture_name [String] Name of capture to remove
|
|
152
|
+
# @return [String] Modified source code
|
|
153
|
+
def remove_matching(source, tree, language, query_pattern:, capture_name: "@item")
|
|
154
|
+
QueryRewriter.new(source, tree, language)
|
|
155
|
+
.query(query_pattern)
|
|
156
|
+
.remove(capture_name)
|
|
157
|
+
.rewrite
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def build_rename_query(kind)
|
|
163
|
+
case kind
|
|
164
|
+
when :function
|
|
165
|
+
<<~QUERY
|
|
166
|
+
[
|
|
167
|
+
(function_item name: (identifier) @name)
|
|
168
|
+
(call_expression function: (identifier) @name)
|
|
169
|
+
]
|
|
170
|
+
QUERY
|
|
171
|
+
when :type
|
|
172
|
+
<<~QUERY
|
|
173
|
+
[
|
|
174
|
+
(struct_item name: (type_identifier) @name)
|
|
175
|
+
(enum_item name: (type_identifier) @name)
|
|
176
|
+
(type_identifier) @name
|
|
177
|
+
]
|
|
178
|
+
QUERY
|
|
179
|
+
when :variable
|
|
180
|
+
"(identifier) @name"
|
|
181
|
+
else
|
|
182
|
+
"(identifier) @name"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def match_has_text?(match, text)
|
|
187
|
+
match.captures.any? { |c| c.node.text == text }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
DECLARATION_TYPES = ["let_declaration", "parameter", "function_item"].freeze
|
|
191
|
+
|
|
192
|
+
def declaration?(match, _source)
|
|
193
|
+
# Check if this identifier is part of a declaration pattern
|
|
194
|
+
match.captures.any? do |c|
|
|
195
|
+
parent = c.node.parent
|
|
196
|
+
next false unless parent
|
|
197
|
+
|
|
198
|
+
# Common declaration patterns
|
|
199
|
+
DECLARATION_TYPES.include?(parent.type)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def find_enclosing_function(node)
|
|
204
|
+
current = node.parent
|
|
205
|
+
while current
|
|
206
|
+
return current if current.type == "function_item"
|
|
207
|
+
|
|
208
|
+
current = current.parent
|
|
209
|
+
end
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TreeSitter
|
|
4
|
+
# Rewriter class for applying edits to parsed source code.
|
|
5
|
+
# Inspired by Parser::TreeRewriter from the parser gem.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# tree = TreeSitter::Parser.new.tap { |p| p.language = "rust" }.parse(source)
|
|
9
|
+
# fn_name = tree.root_node.child(0).child_by_field_name("name")
|
|
10
|
+
#
|
|
11
|
+
# new_source = TreeSitter::Rewriter.new(source, tree)
|
|
12
|
+
# .replace(fn_name, "new_name")
|
|
13
|
+
# .rewrite
|
|
14
|
+
#
|
|
15
|
+
class Rewriter
|
|
16
|
+
# Represents a single edit operation
|
|
17
|
+
Edit = Struct.new(:start_byte, :end_byte, :replacement, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
attr_reader :source, :tree, :edits
|
|
20
|
+
|
|
21
|
+
# Initialize a new Rewriter
|
|
22
|
+
#
|
|
23
|
+
# @param source [String] The source code to rewrite
|
|
24
|
+
# @param tree [TreeSitter::Tree, nil] Optional parsed tree (will parse if not provided)
|
|
25
|
+
# @param parser [TreeSitter::Parser, nil] Optional parser for re-parsing (needed if tree not provided)
|
|
26
|
+
def initialize(source, tree = nil, parser: nil)
|
|
27
|
+
@source = source.dup.freeze
|
|
28
|
+
@tree = tree
|
|
29
|
+
@parser = parser
|
|
30
|
+
@edits = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Remove the text at the given node or range
|
|
34
|
+
#
|
|
35
|
+
# @param node_or_range [TreeSitter::Node, TreeSitter::Range] The node or range to remove
|
|
36
|
+
# @return [self] Returns self for method chaining
|
|
37
|
+
def remove(node_or_range)
|
|
38
|
+
replace(node_or_range, "")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Replace the text at the given node or range with new content
|
|
42
|
+
#
|
|
43
|
+
# @param node_or_range [TreeSitter::Node, TreeSitter::Range] The node or range to replace
|
|
44
|
+
# @param content [String] The replacement content
|
|
45
|
+
# @return [self] Returns self for method chaining
|
|
46
|
+
def replace(node_or_range, content)
|
|
47
|
+
range = normalize_range(node_or_range)
|
|
48
|
+
@edits << Edit.new(
|
|
49
|
+
start_byte: range.start_byte,
|
|
50
|
+
end_byte: range.end_byte,
|
|
51
|
+
replacement: content.to_s,
|
|
52
|
+
)
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Insert text before the given node or range
|
|
57
|
+
#
|
|
58
|
+
# @param node_or_range [TreeSitter::Node, TreeSitter::Range] The node or range
|
|
59
|
+
# @param content [String] The content to insert
|
|
60
|
+
# @return [self] Returns self for method chaining
|
|
61
|
+
def insert_before(node_or_range, content)
|
|
62
|
+
range = normalize_range(node_or_range)
|
|
63
|
+
@edits << Edit.new(
|
|
64
|
+
start_byte: range.start_byte,
|
|
65
|
+
end_byte: range.start_byte,
|
|
66
|
+
replacement: content.to_s,
|
|
67
|
+
)
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Insert text after the given node or range
|
|
72
|
+
#
|
|
73
|
+
# @param node_or_range [TreeSitter::Node, TreeSitter::Range] The node or range
|
|
74
|
+
# @param content [String] The content to insert
|
|
75
|
+
# @return [self] Returns self for method chaining
|
|
76
|
+
def insert_after(node_or_range, content)
|
|
77
|
+
range = normalize_range(node_or_range)
|
|
78
|
+
@edits << Edit.new(
|
|
79
|
+
start_byte: range.end_byte,
|
|
80
|
+
end_byte: range.end_byte,
|
|
81
|
+
replacement: content.to_s,
|
|
82
|
+
)
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Wrap the node or range with before and after text
|
|
87
|
+
#
|
|
88
|
+
# @param node_or_range [TreeSitter::Node, TreeSitter::Range] The node or range to wrap
|
|
89
|
+
# @param before_text [String] Text to insert before
|
|
90
|
+
# @param after_text [String] Text to insert after
|
|
91
|
+
# @return [self] Returns self for method chaining
|
|
92
|
+
def wrap(node_or_range, before_text, after_text)
|
|
93
|
+
insert_before(node_or_range, before_text)
|
|
94
|
+
insert_after(node_or_range, after_text)
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Apply all accumulated edits and return the new source code
|
|
99
|
+
#
|
|
100
|
+
# Edits are applied in reverse order (from end to start) to preserve
|
|
101
|
+
# byte positions of earlier edits.
|
|
102
|
+
#
|
|
103
|
+
# @return [String] The rewritten source code
|
|
104
|
+
def rewrite
|
|
105
|
+
# Sort edits by position descending to apply from end to start
|
|
106
|
+
# This prevents earlier edits from invalidating later positions
|
|
107
|
+
sorted = @edits.sort_by { |e| [-e.start_byte, -e.end_byte] }
|
|
108
|
+
|
|
109
|
+
result = @source.dup
|
|
110
|
+
sorted.each do |edit|
|
|
111
|
+
result[edit.start_byte...edit.end_byte] = edit.replacement
|
|
112
|
+
end
|
|
113
|
+
result
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Apply edits and return both the new source and a new parse tree
|
|
117
|
+
#
|
|
118
|
+
# @return [Array<String, TreeSitter::Tree>] The new source and tree
|
|
119
|
+
# @raise [RuntimeError] If no parser is available for re-parsing
|
|
120
|
+
def rewrite_with_tree
|
|
121
|
+
new_source = rewrite
|
|
122
|
+
|
|
123
|
+
parser = @parser || create_parser_from_tree
|
|
124
|
+
raise "No parser available for re-parsing" unless parser
|
|
125
|
+
|
|
126
|
+
new_tree = parser.parse(new_source)
|
|
127
|
+
[new_source, new_tree]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def normalize_range(node_or_range)
|
|
133
|
+
case node_or_range
|
|
134
|
+
when TreeSitter::Node
|
|
135
|
+
node_or_range.range
|
|
136
|
+
when TreeSitter::Range
|
|
137
|
+
node_or_range
|
|
138
|
+
else
|
|
139
|
+
raise ArgumentError,
|
|
140
|
+
"Expected TreeSitter::Node or TreeSitter::Range, got #{node_or_range.class}"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def create_parser_from_tree
|
|
145
|
+
return unless @tree
|
|
146
|
+
|
|
147
|
+
parser = TreeSitter::Parser.new
|
|
148
|
+
lang = @tree.language
|
|
149
|
+
parser.language = lang.name if lang
|
|
150
|
+
parser
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|