Skip to main content

lasr/
finder.rs

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    // where we matched within the string
22    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        // edits must be applied in reverse to avoid offset issues
218        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        // ast-grep only performs the outer replacement
279        let expected = "foo(32, bar(s, y, baz()), 5)";
280        assert_eq!(expected, actual);
281    }
282}