Skip to main content

thread_rule_engine/
fixer.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
7use crate::DeserializeEnv;
8use crate::maybe::Maybe;
9use crate::rule::{Relation, Rule, RuleSerializeError, StopBy};
10use crate::transform::Transformation;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14use thread_ast_engine::replacer::{Content, Replacer, TemplateFix, TemplateFixError};
15use thread_ast_engine::{Doc, Language, Matcher, NodeMatch};
16
17use std::ops::Range;
18use thread_utilities::{RapidMap, RapidSet};
19
20/// A pattern string or fix object to auto fix the issue.
21/// It can reference metavariables appeared in rule.
22#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
23#[serde(untagged)]
24pub enum SerializableFixer {
25    Str(String),
26    Config(Box<SerializableFixConfig>),
27    List(Vec<SerializableFixConfig>),
28}
29
30#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
31#[serde(rename_all = "camelCase")]
32pub struct SerializableFixConfig {
33    template: String,
34    #[serde(default, skip_serializing_if = "Maybe::is_absent")]
35    expand_end: Maybe<Relation>,
36    #[serde(default, skip_serializing_if = "Maybe::is_absent")]
37    expand_start: Maybe<Relation>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    title: Option<String>,
40}
41
42#[derive(Error, Debug)]
43pub enum FixerError {
44    #[error("Fixer template is invalid.")]
45    InvalidTemplate(#[from] TemplateFixError),
46    #[error("Fixer expansion contains invalid rule.")]
47    WrongExpansion(#[from] RuleSerializeError),
48    #[error("Rewriter must have exactly one fixer.")]
49    InvalidRewriter,
50    #[error("Fixer in list must have title.")]
51    MissingTitle,
52}
53
54#[derive(Clone, Debug)]
55struct Expansion {
56    matches: Rule,
57    stop_by: StopBy,
58}
59
60impl Expansion {
61    fn parse<L: Language>(
62        relation: &Maybe<Relation>,
63        env: &DeserializeEnv<L>,
64    ) -> Result<Option<Self>, FixerError> {
65        let inner = match relation {
66            Maybe::Absent => return Ok(None),
67            Maybe::Present(r) => r.clone(),
68        };
69        let stop_by = StopBy::try_from(inner.stop_by, env)?;
70        let matches = env.deserialize_rule(inner.rule)?;
71        Ok(Some(Self { matches, stop_by }))
72    }
73}
74
75#[derive(Clone, Debug)]
76pub struct Fixer {
77    template: TemplateFix,
78    expand_start: Option<Expansion>,
79    expand_end: Option<Expansion>,
80    title: Option<String>,
81}
82
83impl Fixer {
84    fn do_parse<L: Language>(
85        serialized: &SerializableFixConfig,
86        env: &DeserializeEnv<L>,
87        transform: &Option<RapidMap<String, Transformation>>,
88    ) -> Result<Self, FixerError> {
89        let SerializableFixConfig {
90            template: fix,
91            expand_end,
92            expand_start,
93            title,
94        } = serialized;
95        let expand_start = Expansion::parse(expand_start, env)?;
96        let expand_end = Expansion::parse(expand_end, env)?;
97        let template = if let Some(trans) = transform {
98            let keys: Vec<std::sync::Arc<str>> = trans
99                .keys()
100                .map(|k| std::sync::Arc::from(k.as_str()))
101                .collect();
102            TemplateFix::with_transform(fix, &env.lang, &keys)
103        } else {
104            TemplateFix::try_new(fix, &env.lang)?
105        };
106        Ok(Self {
107            template,
108            expand_start,
109            expand_end,
110            title: title.clone(),
111        })
112    }
113
114    pub fn parse<L: Language>(
115        fixer: &SerializableFixer,
116        env: &DeserializeEnv<L>,
117        transform: &Option<RapidMap<String, Transformation>>,
118    ) -> Result<Vec<Self>, FixerError> {
119        let ret = match fixer {
120            SerializableFixer::Str(fix) => Self::with_transform(fix, env, transform),
121            SerializableFixer::Config(cfg) => Self::do_parse(cfg, env, transform),
122            SerializableFixer::List(list) => {
123                return Self::parse_list(list, env, transform);
124            }
125        };
126        Ok(vec![ret?])
127    }
128
129    fn parse_list<L: Language>(
130        list: &[SerializableFixConfig],
131        env: &DeserializeEnv<L>,
132        transform: &Option<RapidMap<String, Transformation>>,
133    ) -> Result<Vec<Self>, FixerError> {
134        list.iter()
135            .map(|cfg| {
136                if cfg.title.is_none() {
137                    return Err(FixerError::MissingTitle);
138                }
139                Self::do_parse(cfg, env, transform)
140            })
141            .collect()
142    }
143
144    pub(crate) fn with_transform<L: Language>(
145        fix: &str,
146        env: &DeserializeEnv<L>,
147        transform: &Option<RapidMap<String, Transformation>>,
148    ) -> Result<Self, FixerError> {
149        let template = if let Some(trans) = transform {
150            let keys: Vec<std::sync::Arc<str>> = trans
151                .keys()
152                .map(|k| std::sync::Arc::from(k.as_str()))
153                .collect();
154            TemplateFix::with_transform(fix, &env.lang, &keys)
155        } else {
156            TemplateFix::try_new(fix, &env.lang)?
157        };
158        Ok(Self {
159            template,
160            expand_end: None,
161            expand_start: None,
162            title: None,
163        })
164    }
165
166    pub fn from_str<L: Language>(src: &str, lang: &L) -> Result<Self, FixerError> {
167        let template = TemplateFix::try_new(src, lang)?;
168        Ok(Self {
169            template,
170            expand_start: None,
171            expand_end: None,
172            title: None,
173        })
174    }
175
176    pub fn title(&self) -> Option<&str> {
177        self.title.as_deref()
178    }
179
180    pub(crate) fn used_vars(&self) -> RapidSet<&str> {
181        self.template.used_vars()
182    }
183}
184
185impl<D, C> Replacer<D> for Fixer
186where
187    D: Doc<Source = C>,
188    C: Content,
189{
190    fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Vec<C::Underlying> {
191        // simple forwarding to template
192        self.template.generate_replacement(nm)
193    }
194    fn get_replaced_range(&self, nm: &NodeMatch<'_, D>, matcher: impl Matcher) -> Range<usize> {
195        let range = nm.range();
196        if self.expand_start.is_none() && self.expand_end.is_none() {
197            return if let Some(len) = matcher.get_match_len(nm.get_node().clone()) {
198                range.start..range.start + len
199            } else {
200                range
201            };
202        }
203        let start = expand_start(self.expand_start.as_ref(), nm);
204        let end = expand_end(self.expand_end.as_ref(), nm);
205        start..end
206    }
207}
208
209fn expand_start<D: Doc>(expansion: Option<&Expansion>, nm: &NodeMatch<'_, D>) -> usize {
210    let node = nm.get_node();
211    let mut env = std::borrow::Cow::Borrowed(nm.get_env());
212    let Some(start) = expansion else {
213        return node.range().start;
214    };
215    let node = start.stop_by.find(
216        || node.prev(),
217        || node.prev_all(),
218        |n| start.matches.match_node_with_env(n, &mut env),
219    );
220    node.map(|n| n.range().start)
221        .unwrap_or_else(|| nm.range().start)
222}
223
224fn expand_end<D: Doc>(expansion: Option<&Expansion>, nm: &NodeMatch<'_, D>) -> usize {
225    let node = nm.get_node();
226    let mut env = std::borrow::Cow::Borrowed(nm.get_env());
227    let Some(end) = expansion else {
228        return node.range().end;
229    };
230    let node = end.stop_by.find(
231        || node.next(),
232        || node.next_all(),
233        |n| end.matches.match_node_with_env(n, &mut env),
234    );
235    node.map(|n| n.range().end)
236        .unwrap_or_else(|| nm.range().end)
237}
238
239#[cfg(test)]
240mod test {
241    use super::*;
242    use crate::from_str;
243    use crate::maybe::Maybe;
244    use crate::test::TypeScript;
245    use thread_ast_engine::tree_sitter::LanguageExt;
246
247    #[test]
248    fn test_parse() {
249        let fixer: SerializableFixer = from_str("test").expect("should parse");
250        assert!(matches!(fixer, SerializableFixer::Str(_)));
251    }
252
253    fn parse(config: SerializableFixConfig) -> Result<Fixer, FixerError> {
254        let config = SerializableFixer::Config(Box::new(config));
255        let env = DeserializeEnv::new(TypeScript::Tsx);
256        let fixer = Fixer::parse(&config, &env, &Some(Default::default()))?.remove(0);
257        Ok(fixer)
258    }
259
260    #[test]
261    fn test_deserialize_object() -> Result<(), serde_yaml::Error> {
262        let src = "{template: 'abc', expandEnd: {regex: ',', stopBy: neighbor}}";
263        let SerializableFixer::Config(cfg) = from_str(src)? else {
264            panic!("wrong parsing")
265        };
266        assert_eq!(cfg.template, "abc");
267        let Maybe::Present(relation) = cfg.expand_end else {
268            panic!("wrong parsing")
269        };
270        let rule = relation.rule;
271        assert_eq!(rule.regex, Maybe::Present(",".to_string()));
272        assert!(rule.pattern.is_absent());
273        Ok(())
274    }
275
276    #[test]
277    fn test_parse_config() -> Result<(), FixerError> {
278        let relation = from_str("{regex: ',', stopBy: neighbor}").expect("should deser");
279        let config = SerializableFixConfig {
280            expand_end: Maybe::Present(relation),
281            expand_start: Maybe::Absent,
282            template: "abcd".to_string(),
283            title: None,
284        };
285        let ret = parse(config)?;
286        assert!(ret.expand_start.is_none());
287        assert!(ret.expand_end.is_some());
288        assert!(matches!(ret.template, TemplateFix::Textual(_)));
289        Ok(())
290    }
291
292    #[test]
293    fn test_parse_str() -> Result<(), FixerError> {
294        let config = SerializableFixer::Str("abcd".to_string());
295        let env = DeserializeEnv::new(TypeScript::Tsx);
296        let ret = Fixer::parse(&config, &env, &None)?.remove(0);
297        assert!(ret.expand_end.is_none());
298        assert!(ret.expand_start.is_none());
299        assert!(matches!(ret.template, TemplateFix::Textual(_)));
300        Ok(())
301    }
302
303    #[test]
304    fn test_replace_fixer() -> Result<(), FixerError> {
305        let expand_end = from_str("{regex: ',', stopBy: neighbor}").expect("should word");
306        let config = SerializableFixConfig {
307            expand_end: Maybe::Present(expand_end),
308            expand_start: Maybe::Absent,
309            template: "var $A = 456".to_string(),
310            title: None,
311        };
312        let fixer = parse(config)?;
313        let grep = TypeScript::Tsx.ast_grep("let a = 123");
314        let node = grep.root().find("let $A = 123").expect("should found");
315        let edit = fixer.generate_replacement(&node);
316        assert_eq!(String::from_utf8_lossy(&edit), "var a = 456");
317        Ok(())
318    }
319
320    #[test]
321    fn test_replace_range() -> Result<(), FixerError> {
322        use thread_ast_engine::matcher::KindMatcher;
323        let expand_end = from_str("{regex: ',', stopBy: neighbor}").expect("should word");
324        let config = SerializableFixConfig {
325            expand_end: Maybe::Present(expand_end),
326            expand_start: Maybe::Absent,
327            template: "c: 456".to_string(),
328            title: None,
329        };
330        let fixer = parse(config)?;
331        let grep = TypeScript::Tsx.ast_grep("var a = { b: 123, }");
332        let matcher = KindMatcher::new("pair", &TypeScript::Tsx);
333        let node = grep.root().find(&matcher).expect("should found");
334        let edit = node.make_edit(&matcher, &fixer);
335        let text = String::from_utf8_lossy(&edit.inserted_text);
336        assert_eq!(text, "c: 456");
337        assert_eq!(edit.position, 10);
338        assert_eq!(edit.deleted_length, 7);
339        Ok(())
340    }
341
342    #[test]
343    fn test_fixer_list() -> Result<(), FixerError> {
344        let config: SerializableFixer = from_str(
345            r"
346- { template: 'abc', title: 'fixer 1'}
347- { template: 'def', title: 'fixer 2'}",
348        )
349        .expect("should parse");
350        let env = DeserializeEnv::new(TypeScript::Tsx);
351        let fixers = Fixer::parse(&config, &env, &Some(Default::default()))?;
352        assert_eq!(fixers.len(), 2);
353        let config: SerializableFixer = from_str(
354            r"
355- { template: 'abc', title: 'fixer 1'}
356- { template: 'def'}",
357        )
358        .expect("should parse");
359        let env = DeserializeEnv::new(TypeScript::Tsx);
360        let ret = Fixer::parse(&config, &env, &Some(Default::default()));
361        assert!(ret.is_err());
362        Ok(())
363    }
364}