1use super::indent::{DeindentedExtract, extract_with_deindent, get_indent_at_offset, indent_lines};
8use super::{MetaVarExtract, Replacer, split_first_meta_var};
9use crate::NodeMatch;
10use crate::language::Language;
11use crate::meta_var::{MetaVarEnv, Underlying};
12use crate::source::{Content, Doc};
13
14use thiserror::Error;
15
16use std::borrow::Cow;
17use thread_utilities::{RapidSet, get_set};
18
19#[derive(Debug, Clone)]
20pub enum TemplateFix {
21 Textual(String),
23 WithMetaVar(Template),
24}
25
26#[derive(Debug, Error)]
27pub enum TemplateFixError {}
28
29impl TemplateFix {
30 pub fn try_new<L: Language>(template: &str, lang: &L) -> Result<Self, TemplateFixError> {
31 Ok(create_template(template, lang.meta_var_char(), &[]))
32 }
33
34 pub fn with_transform<L: Language>(
35 tpl: &str,
36 lang: &L,
37 trans: &[crate::meta_var::MetaVariableID],
38 ) -> Self {
39 create_template(tpl, lang.meta_var_char(), trans)
40 }
41
42 #[must_use]
43 pub fn used_vars(&self) -> RapidSet<&str> {
44 let template = match self {
45 Self::WithMetaVar(t) => t,
46 Self::Textual(_) => return get_set(),
47 };
48 template.vars.iter().map(|v| v.0.used_var()).collect()
49 }
50}
51
52impl<D: Doc> Replacer<D> for TemplateFix {
53 fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Underlying<D> {
54 let leading = nm.get_doc().get_source().get_range(0..nm.range().start);
55 let indent = get_indent_at_offset::<D::Source>(leading);
56 let bytes = replace_fixer(self, nm.get_env());
57 let replaced = DeindentedExtract::MultiLine(&bytes, 0);
58 indent_lines::<D::Source>(indent, &replaced).to_vec()
59 }
60}
61
62type Indent = usize;
63
64#[derive(Debug, Clone)]
65pub struct Template {
66 fragments: Vec<String>,
67 vars: Vec<(MetaVarExtract, Indent)>,
68}
69
70fn create_template(
71 tmpl: &str,
72 mv_char: char,
73 transforms: &[crate::meta_var::MetaVariableID],
74) -> TemplateFix {
75 let mut fragments = vec![];
76 let mut vars = vec![];
77 let mut offset = 0;
78 let mut len = 0;
79 while let Some(i) = tmpl[len + offset..].find(mv_char) {
80 if let Some((meta_var, skipped)) =
81 split_first_meta_var(&tmpl[len + offset + i..], mv_char, transforms)
82 {
83 fragments.push(tmpl[len..len + offset + i].to_string());
84 let indent = get_indent_at_offset::<String>(&tmpl.as_bytes()[..len + offset + i]);
86 vars.push((meta_var, indent));
87 len += skipped + offset + i;
88 offset = 0;
89 continue;
90 }
91 debug_assert!(len + offset + i < tmpl.len());
92 offset = offset + i + 1;
96 }
97 if fragments.is_empty() {
98 TemplateFix::Textual(tmpl[len..].to_string())
99 } else {
100 fragments.push(tmpl[len..].to_string());
101 TemplateFix::WithMetaVar(Template { fragments, vars })
102 }
103}
104
105fn replace_fixer<D: Doc>(fixer: &TemplateFix, env: &MetaVarEnv<'_, D>) -> Underlying<D> {
106 let template = match fixer {
107 TemplateFix::Textual(n) => return D::Source::decode_str(n).to_vec(),
108 TemplateFix::WithMetaVar(t) => t,
109 };
110 let mut ret = vec![];
111 let mut frags = template.fragments.iter();
112 let vars = template.vars.iter();
113 if let Some(frag) = frags.next() {
114 ret.extend_from_slice(&D::Source::decode_str(frag));
115 }
116 for ((var, indent), frag) in vars.zip(frags) {
117 if let Some(bytes) = maybe_get_var(env, var, indent.to_owned()) {
118 ret.extend_from_slice(&bytes);
119 }
120 ret.extend_from_slice(&D::Source::decode_str(frag));
121 }
122 ret
123}
124
125fn maybe_get_var<'e, 't, C, D>(
126 env: &'e MetaVarEnv<'t, D>,
127 var: &MetaVarExtract,
128 indent: usize,
129) -> Option<Cow<'e, [C::Underlying]>>
130where
131 C: Content + 'e,
132 D: Doc<Source = C>,
133{
134 let (source, range) = match var {
135 MetaVarExtract::Transformed(name) => {
136 let source = env.get_transformed(name)?;
138 let de_intended = DeindentedExtract::MultiLine(source, 0);
139 let bytes = indent_lines::<D::Source>(indent, &de_intended);
140 return Some(Cow::Owned(bytes.into()));
141 }
142 MetaVarExtract::Single(name) => {
143 let replaced = env.get_match(name)?;
144 let source = replaced.get_doc().get_source();
145 let range = replaced.range();
146 (source, range)
147 }
148 MetaVarExtract::Multiple(name) => {
149 let nodes = env.get_multiple_matches(name);
150 if nodes.is_empty() {
151 return None;
152 }
153 let start = nodes[0].range().start;
157 let end = nodes[nodes.len() - 1].range().end;
158 let source = nodes[0].get_doc().get_source();
159 (source, start..end)
160 }
161 };
162 let extracted = extract_with_deindent(source, range);
163 let bytes = indent_lines::<D::Source>(indent, &extracted);
164 Some(Cow::Owned(bytes.into()))
165}
166
167pub fn gen_replacement<D: Doc>(template: &str, nm: &NodeMatch<'_, D>) -> Underlying<D> {
169 let fixer = create_template(template, nm.lang().meta_var_char(), &[]);
170 fixer.generate_replacement(nm)
171}
172
173#[cfg(test)]
174mod test {
175
176 use super::*;
177 use crate::Pattern;
178 use crate::language::Tsx;
179 use crate::matcher::NodeMatch;
180 use crate::meta_var::{MetaVarEnv, MetaVariable};
181 use crate::tree_sitter::LanguageExt;
182 use std::sync::Arc;
183 use thread_utilities::RapidMap;
184
185 #[test]
186 fn test_example() {
187 let src = r"
188if (true) {
189 a(
190 1
191 + 2
192 + 3
193 )
194}";
195 let pattern = "a($B)";
196 let template = r"c(
197 $B
198)";
199 let mut src = Tsx.ast_grep(src);
200 let pattern = Pattern::new(pattern, &Tsx);
201 let success = src.replace(pattern, template).expect("should replace");
202 assert!(success);
203 let expect = r"if (true) {
204 c(
205 1
206 + 2
207 + 3
208 )
209}";
210 assert_eq!(src.root().text(), expect);
211 }
212
213 fn test_str_replace(replacer: &str, vars: &[(&str, &str)], expected: &str) {
214 let mut env = MetaVarEnv::new();
215 let roots: Vec<_> = vars.iter().map(|(v, p)| (v, Tsx.ast_grep(p))).collect();
216 for (var, root) in &roots {
217 env.insert(var, root.root());
218 }
219 let dummy = Tsx.ast_grep("dummy");
220 let node_match = NodeMatch::new(dummy.root(), env.clone());
221 let replaced = replacer.generate_replacement(&node_match);
222 let replaced = String::from_utf8_lossy(&replaced);
223 assert_eq!(
224 replaced,
225 expected,
226 "wrong replacement {replaced} {expected} {:?}",
227 RapidMap::from(env)
228 );
229 }
230
231 #[test]
232 fn test_no_env() {
233 test_str_replace("let a = 123", &[], "let a = 123");
234 test_str_replace(
235 "console.log('hello world'); let b = 123;",
236 &[],
237 "console.log('hello world'); let b = 123;",
238 );
239 }
240
241 #[test]
242 fn test_single_env() {
243 test_str_replace("let a = $A", &[("A", "123")], "let a = 123");
244 test_str_replace(
245 "console.log($HW); let b = 123;",
246 &[("HW", "'hello world'")],
247 "console.log('hello world'); let b = 123;",
248 );
249 }
250
251 #[test]
252 fn test_multiple_env() {
253 test_str_replace("let $V = $A", &[("A", "123"), ("V", "a")], "let a = 123");
254 test_str_replace(
255 "console.log($HW); let $B = 123;",
256 &[("HW", "'hello world'"), ("B", "b")],
257 "console.log('hello world'); let b = 123;",
258 );
259 }
260
261 #[test]
262 fn test_multiple_occurrences() {
263 test_str_replace("let $A = $A", &[("A", "a")], "let a = a");
264 test_str_replace("var $A = () => $A", &[("A", "a")], "var a = () => a");
265 test_str_replace(
266 "const $A = () => { console.log($B); $A(); };",
267 &[("B", "'hello world'"), ("A", "a")],
268 "const a = () => { console.log('hello world'); a(); };",
269 );
270 }
271
272 fn test_ellipsis_replace(replacer: &str, vars: &[(&str, &str)], expected: &str) {
273 let mut env = MetaVarEnv::new();
274 let roots: Vec<_> = vars.iter().map(|(v, p)| (v, Tsx.ast_grep(p))).collect();
275 for (var, root) in &roots {
276 env.insert_multi(var, root.root().children().collect());
277 }
278 let dummy = Tsx.ast_grep("dummy");
279 let node_match = NodeMatch::new(dummy.root(), env.clone());
280 let replaced = replacer.generate_replacement(&node_match);
281 let replaced = String::from_utf8_lossy(&replaced);
282 assert_eq!(
283 replaced,
284 expected,
285 "wrong replacement {replaced} {expected} {:?}",
286 RapidMap::from(env)
287 );
288 }
289
290 #[test]
291 fn test_ellipsis_meta_var() {
292 test_ellipsis_replace(
293 "let a = () => { $$$B }",
294 &[("B", "alert('works!')")],
295 "let a = () => { alert('works!') }",
296 );
297 test_ellipsis_replace(
298 "let a = () => { $$$B }",
299 &[("B", "alert('works!');console.log(123)")],
300 "let a = () => { alert('works!');console.log(123) }",
301 );
302 }
303
304 #[test]
305 fn test_multi_ellipsis() {
306 test_ellipsis_replace(
307 "import {$$$A, B, $$$C} from 'a'",
308 &[("A", "A"), ("C", "C")],
309 "import {A, B, C} from 'a'",
310 );
311 }
312
313 #[test]
314 fn test_replace_in_string() {
315 test_str_replace("'$A'", &[("A", "123")], "'123'");
316 }
317
318 fn test_template_replace(template: &str, vars: &[(&str, &str)], expected: &str) {
319 let mut env = MetaVarEnv::new();
320 let roots: Vec<_> = vars.iter().map(|(v, p)| (v, Tsx.ast_grep(p))).collect();
321 for (var, root) in &roots {
322 env.insert(var, root.root());
323 }
324 let dummy = Tsx.ast_grep("dummy");
325 let node_match = NodeMatch::new(dummy.root(), env.clone());
326 let bytes = template.generate_replacement(&node_match);
327 let ret = String::from_utf8(bytes).expect("replacement must be valid utf-8");
328 assert_eq!(expected, ret);
329 }
330
331 #[test]
332 fn test_template() {
333 test_template_replace("Hello $A", &[("A", "World")], "Hello World");
334 test_template_replace("$B $A", &[("A", "World"), ("B", "Hello")], "Hello World");
335 }
336
337 #[test]
338 fn test_template_vars() {
339 let tf = TemplateFix::try_new("$A $B $C", &Tsx).expect("ok");
340 assert_eq!(tf.used_vars(), ["A", "B", "C"].into_iter().collect());
341 let tf = TemplateFix::try_new("$a$B$C", &Tsx).expect("ok");
342 assert_eq!(tf.used_vars(), ["B", "C"].into_iter().collect());
343 let tf = TemplateFix::try_new("$a$B$C", &Tsx).expect("ok");
344 assert_eq!(tf.used_vars(), ["B", "C"].into_iter().collect());
345 }
346
347 #[test]
349 fn test_multi_row_replace() {
350 test_template_replace(
351 "$A = $B",
352 &[("A", "x"), ("B", "[\n 1\n]")],
353 "x = [\n 1\n]",
354 );
355 }
356
357 #[test]
358 fn test_replace_rewriter() {
359 let tf = TemplateFix::with_transform("if (a)\n $A", &Tsx, &[Arc::from("A")]);
360 let mut env = MetaVarEnv::new();
361 env.insert_transformation(
362 &MetaVariable::Multiple,
363 "A",
364 "if (b)\n foo".bytes().collect(),
365 );
366 let dummy = Tsx.ast_grep("dummy");
367 let node_match = NodeMatch::new(dummy.root(), env.clone());
368 let bytes = tf.generate_replacement(&node_match);
369 let ret = String::from_utf8(bytes).expect("replacement must be valid utf-8");
370 assert_eq!("if (a)\n if (b)\n foo", ret);
371 }
372
373 #[test]
374 fn test_nested_matching_replace() {
375 }
377}