1use anyhow::{Context, Result, bail};
2use ast_grep_core::{Pattern, language::Language};
3use ast_grep_language::{LanguageExt, SupportLang};
4use grep::{
5 regex::{RegexMatcher, RegexMatcherBuilder},
6 searcher::{BinaryDetection, Searcher, SearcherBuilder, sinks},
7};
8use regex::{Regex, RegexBuilder};
9use std::{
10 ops::Range,
11 path::{Path, PathBuf},
12 sync::OnceLock,
13};
14use tracing::trace;
15
16#[derive(Debug, PartialEq)]
17pub struct LineMatch {
18 pub number: u64,
19 pub text: String,
20
21 pub ranges: Vec<Range<usize>>,
23}
24
25#[derive(Debug, PartialEq)]
26pub struct FileMatch {
27 pub path: PathBuf,
28 pub lines: Vec<LineMatch>,
29}
30
31#[derive(Debug, Clone)]
32pub struct SearchParams {
33 pub paths: Vec<PathBuf>,
34 pub types: ignore::types::Types,
35 pub threads: usize,
36}
37
38#[derive(Debug, Clone)]
39pub struct RegexParams {
40 pub ignore_case: bool,
41 pub multi_line: bool,
42}
43
44#[derive(Debug, Clone)]
45pub enum Finder {
46 Regex(Box<RegexFinder>),
47 Ast(AstFinder),
48}
49
50fn is_ast_pattern(pattern: &str) -> bool {
51 static REGEX: OnceLock<Regex> = OnceLock::new();
52 let re = REGEX.get_or_init(|| Regex::new("\\$[A-Z_][A-Z_0-9]*|\\$\\$\\$").unwrap());
53 re.is_match(pattern)
54}
55
56#[test]
57fn test_is_ast_pattern() {
58 assert!(is_ast_pattern("let $X ="));
59 assert!(is_ast_pattern("fn($$$ARGS)"));
60 assert!(!is_ast_pattern("^foo$"));
61 assert!(!is_ast_pattern("foo"));
62 assert!(!is_ast_pattern("foo.*"));
63 assert!(!is_ast_pattern("foo(.*)"));
64}
65
66impl Finder {
67 pub fn new(pattern: &str, params: &RegexParams) -> Option<Self> {
68 if is_ast_pattern(pattern) {
69 return Some(Self::Ast(AstFinder::new(pattern)));
70 }
71 match RegexFinder::new(pattern, params) {
72 Ok(f) => Some(Self::Regex(Box::new(f))),
73 Err(e) => {
74 trace!("Not a valid regex pattern: {pattern}: {e}");
75 None
76 }
77 }
78 }
79
80 pub fn find(&mut self, path: &Path) -> Result<Vec<LineMatch>> {
81 match self {
82 Finder::Regex(f) => f.find(path),
83 Finder::Ast(f) => f.find(path),
84 }
85 }
86
87 pub fn replace(&self, path: &Path, text: &str, replacement: &str) -> Result<String> {
88 match self {
89 Finder::Regex(f) => f.replace(text, replacement),
90 Finder::Ast(f) => f.replace(path, text, replacement),
91 }
92 }
93}
94
95#[derive(Clone, Debug)]
96pub struct RegexFinder {
97 regex: Regex,
98 matcher: RegexMatcher,
99 searcher: Searcher,
100}
101
102impl RegexFinder {
103 fn new(pattern: &str, params: &RegexParams) -> Result<Self> {
104 let regex = RegexBuilder::new(pattern)
105 .case_insensitive(params.ignore_case)
106 .build()
107 .with_context(|| format!("Invalid regex: {pattern}"))?;
108
109 let matcher = RegexMatcherBuilder::new()
110 .case_smart(false)
111 .case_insensitive(params.ignore_case)
112 .multi_line(params.multi_line)
113 .build(pattern)
114 .with_context(|| format!("Failed to compile searcher with params: {params:?}"))?;
115
116 let searcher = SearcherBuilder::new()
117 .binary_detection(BinaryDetection::quit(0))
118 .multi_line(params.multi_line)
119 .build();
120
121 Ok(Self {
122 regex,
123 matcher,
124 searcher,
125 })
126 }
127
128 fn find(&mut self, path: &Path) -> Result<Vec<LineMatch>> {
129 let mut lines = vec![];
130 self.searcher.search_path(
131 &self.matcher,
132 path,
133 sinks::UTF8(|number, text| {
134 lines.push(LineMatch {
135 number,
136 text: text.to_string(),
137 ranges: self
138 .regex
139 .find_iter(text)
140 .map(|m| m.start()..m.end())
141 .collect(),
142 });
143 Ok(true)
144 }),
145 )?;
146
147 Ok(lines)
148 }
149
150 pub fn replace(&self, text: &str, replacement: &str) -> Result<String> {
151 Ok(self.regex.replace_all(text, replacement).to_string())
152 }
153}
154
155#[derive(Clone, Debug)]
156pub struct AstFinder {
157 pattern: String,
158}
159
160impl AstFinder {
161 pub fn new(pattern: impl Into<String>) -> Self {
162 Self {
163 pattern: pattern.into(),
164 }
165 }
166
167 fn find(&mut self, path: &Path) -> Result<Vec<LineMatch>> {
168 let Some(lang) = SupportLang::from_path(path) else {
169 trace!("No AST language for {path:?}");
170 return Ok(vec![]);
171 };
172
173 let pattern = match Pattern::try_new(&self.pattern, lang) {
174 Ok(p) => p,
175 Err(e) => {
176 trace!("Invalid pattern for language {lang:?}: {e}");
177 return Ok(vec![]);
178 }
179 };
180
181 trace!(
182 "reading {path:?} of lang {lang} with pattern {}",
183 self.pattern
184 );
185 let src = std::fs::read_to_string(path).with_context(|| format!("Reading {path:?}"))?;
186 let root = lang.ast_grep(src);
187 let node = root.root();
188
189 Ok(node
190 .find_all(pattern)
191 .map(|m| {
192 let text = m.text();
193 LineMatch {
194 number: m.start_pos().line() as u64,
195 ranges: vec![Range {
196 start: 0,
197 end: text.len(),
198 }],
199 text: text.into(),
200 }
201 })
202 .collect())
203 }
204
205 fn replace(&self, path: &Path, text: &str, replacement: &str) -> Result<String> {
206 let lang =
207 SupportLang::from_path(path).with_context(|| format!("No language for {path:?}"))?;
208
209 let pattern = Pattern::try_new(&self.pattern, lang)
210 .with_context(|| format!("Invalid pattern for language {lang:?}"))?;
211
212 let mut root = lang.ast_grep(text);
213 let node = root.root();
214
215 let edits = node.replace_all(&pattern, replacement);
216
217 for edit in edits.into_iter().rev() {
219 if let Err(e) = root.edit(edit) {
220 bail!("Failed to edit {path:?}: {e}");
221 }
222 }
223 Ok(root.generate())
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use pretty_assertions::assert_eq;
231
232 #[test]
233 fn test_ast_replace() {
234 let finder = Finder::new(
235 "$FN($$$ARGS)",
236 &RegexParams {
237 ignore_case: true,
238 multi_line: true,
239 },
240 )
241 .unwrap();
242 let src = "\
243def thing(x, y):
244 print(x + y)
245
246
247thing(3, 5)
248";
249 let actual = finder
250 .replace(Path::new("example.py"), src, "$FN($$$ARGS, 5)")
251 .unwrap();
252
253 let expected = "\
254def thing(x, y):
255 print(x + y, 5)
256
257
258thing(3, 5, 5)
259";
260 assert_eq!(expected, actual);
261 }
262
263 #[test]
264 fn test_ast_replace_overlapped() {
265 let finder = Finder::new(
266 "$FN($$$ARGS)",
267 &RegexParams {
268 ignore_case: true,
269 multi_line: true,
270 },
271 )
272 .unwrap();
273 let src = "foo(32, bar(s, y, baz()))";
274 let actual = finder
275 .replace(Path::new("example.py"), src, "$FN($$$ARGS, 5)")
276 .unwrap();
277
278 let expected = "foo(32, bar(s, y, baz()), 5)";
280 assert_eq!(expected, actual);
281 }
282}