Skip to main content

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}