thread_ast_engine/replacer.rs
1// SPDX-FileCopyrightText: 2022 Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com>
2// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
3// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
4//
5// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
6
7//! # Code Replacement and Transformation
8//!
9//! Tools for replacing and transforming matched AST nodes with new content.
10//!
11//! ## Core Concepts
12//!
13//! - [`Replacer`] - Trait for generating replacement content from matched nodes
14//! - Template-based replacement using meta-variables (e.g., `"let $VAR = $VALUE"`)
15//! - Structural replacement using other AST nodes
16//! - Automatic indentation handling to preserve code formatting
17//!
18//! ## Built-in Replacers
19//!
20//! Several types implement [`Replacer`] out of the box:
21//!
22//! - **`&str`** - Template strings with meta-variable substitution
23//! - **[`Root`]** - Replace with entire AST trees
24//! - **[`Node`]** - Replace with specific nodes
25//!
26//! ## Examples
27//!
28//! ### Template Replacement
29//!
30//! ```rust,no_run
31//! # use thread_ast_engine::Language;
32//! # use thread_ast_engine::tree_sitter::LanguageExt;
33//! # use thread_ast_engine::matcher::MatcherExt;
34//! let mut ast = Language::Tsx.ast_grep("var x = 42;");
35//!
36//! // Replace using a template string
37//! ast.replace("var $NAME = $VALUE", "const $NAME = $VALUE");
38//! println!("{}", ast.generate()); // "const x = 42;"
39//! ```
40//!
41//! ### Structural Replacement
42//!
43//! ```rust,no_run
44//! # use thread_ast_engine::Language;
45//! # use thread_ast_engine::tree_sitter::LanguageExt;
46//! # use thread_ast_engine::matcher::MatcherExt;
47//! let mut target = Language::Tsx.ast_grep("old_function();");
48//! let replacement = Language::Tsx.ast_grep("new_function(42)");
49//!
50//! // Replace with another AST
51//! target.replace("old_function()", replacement);
52//! println!("{}", target.generate()); // "new_function(42);"
53//! ```
54
55use crate::matcher::Matcher;
56use crate::meta_var::{MetaVariableID, Underlying, is_valid_meta_var_char};
57use crate::{Doc, Node, NodeMatch, Root};
58use std::ops::Range;
59use std::sync::Arc;
60
61pub(crate) use indent::formatted_slice;
62
63use crate::source::Edit as E;
64type Edit<D> = E<<D as Doc>::Source>;
65
66mod indent;
67mod structural;
68mod template;
69
70pub use crate::source::Content;
71pub use template::{TemplateFix, TemplateFixError};
72
73/// Generate replacement content for matched AST nodes.
74///
75/// The `Replacer` trait defines how to transform a matched node into new content.
76/// Implementations can use template strings with meta-variables, structural
77/// replacement with other AST nodes, or custom logic.
78///
79/// # Type Parameters
80///
81/// - `D: Doc` - The document type containing source code and language information
82///
83/// # Example Implementation
84///
85/// ```rust,no_run
86/// # use thread_ast_engine::replacer::Replacer;
87/// # use thread_ast_engine::{Doc, NodeMatch};
88/// # use thread_ast_engine::meta_var::Underlying;
89/// struct CustomReplacer;
90///
91/// impl<D: Doc> Replacer<D> for CustomReplacer {
92/// fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Underlying<D> {
93/// // Custom replacement logic here
94/// "new_code".as_bytes().to_vec()
95/// }
96/// }
97/// ```
98pub trait Replacer<D: Doc> {
99 /// Generate replacement content for a matched node.
100 ///
101 /// Takes a [`NodeMatch`] containing the matched node and its captured
102 /// meta-variables, then returns the raw bytes that should replace the
103 /// matched content in the source code.
104 ///
105 /// # Parameters
106 ///
107 /// - `nm` - The matched node with captured meta-variables
108 ///
109 /// # Returns
110 ///
111 /// Raw bytes representing the replacement content
112 fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Underlying<D>;
113
114 /// Determine the exact range of source code to replace.
115 ///
116 /// By default, replaces the entire matched node's range. Some matchers
117 /// may want to replace only a portion of the matched content.
118 ///
119 /// # Parameters
120 ///
121 /// - `nm` - The matched node
122 /// - `matcher` - The matcher that found this node (may provide custom range info)
123 ///
124 /// # Returns
125 ///
126 /// Byte range in the source code to replace
127 fn get_replaced_range(&self, nm: &NodeMatch<'_, D>, matcher: impl Matcher) -> Range<usize> {
128 let range = nm.range();
129 if let Some(len) = matcher.get_match_len(nm.get_node().clone()) {
130 range.start..range.start + len
131 } else {
132 range
133 }
134 }
135}
136
137impl<D: Doc> Replacer<D> for str {
138 fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Underlying<D> {
139 template::gen_replacement(self, nm)
140 }
141}
142
143impl<D: Doc> Replacer<D> for Root<D> {
144 fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Underlying<D> {
145 structural::gen_replacement(self, nm)
146 }
147}
148
149impl<D, T> Replacer<D> for &T
150where
151 D: Doc,
152 T: Replacer<D> + ?Sized,
153{
154 fn generate_replacement(&self, nm: &NodeMatch<D>) -> Underlying<D> {
155 (**self).generate_replacement(nm)
156 }
157}
158
159impl<D: Doc> Replacer<D> for Node<'_, D> {
160 fn generate_replacement(&self, _nm: &NodeMatch<'_, D>) -> Underlying<D> {
161 let range = self.range();
162 self.root.doc.get_source().get_range(range).to_vec()
163 }
164}
165
166#[derive(Debug, Clone)]
167enum MetaVarExtract {
168 /// $A for captured meta var
169 Single(MetaVariableID),
170 /// $$$A for captured ellipsis
171 Multiple(MetaVariableID),
172 Transformed(MetaVariableID),
173}
174
175impl MetaVarExtract {
176 fn used_var(&self) -> &str {
177 match self {
178 Self::Single(s) | Self::Multiple(s) | Self::Transformed(s) => s,
179 }
180 }
181}
182
183fn split_first_meta_var(
184 src: &str,
185 meta_char: char,
186 transform: &[MetaVariableID],
187) -> Option<(MetaVarExtract, usize)> {
188 debug_assert!(src.starts_with(meta_char));
189 let mut i = 0;
190 let mut skipped = 0;
191 let is_multi = loop {
192 i += 1;
193 skipped += meta_char.len_utf8();
194 if i == 3 {
195 break true;
196 }
197 if !src[skipped..].starts_with(meta_char) {
198 break false;
199 }
200 };
201 // no Anonymous meta var allowed, so _ is not allowed
202 let i = src[skipped..]
203 .find(|c: char| !is_valid_meta_var_char(c))
204 .unwrap_or(src.len() - skipped);
205 // no name found
206 if i == 0 {
207 return None;
208 }
209 let name: MetaVariableID = Arc::from(&src[skipped..skipped + i]);
210 let var = if is_multi {
211 MetaVarExtract::Multiple(name)
212 } else if transform.contains(&name) {
213 MetaVarExtract::Transformed(name)
214 } else {
215 MetaVarExtract::Single(name)
216 };
217 Some((var, skipped + i))
218}