ast_grep_config/
fixer.rs

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