tree_sitter 0.1.0-x86_64-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.
@@ -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