ide_ssr/
lib.rs

1//! Structural Search Replace
2//!
3//! Allows searching the AST for code that matches one or more patterns and then replacing that code
4//! based on a template.
5
6// Feature: Structural Search and Replace
7//
8// Search and replace with named wildcards that will match any expression, type, path, pattern or item.
9// The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`.
10// A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement.
11// Within a macro call, a placeholder will match up until whatever token follows the placeholder.
12//
13// All paths in both the search pattern and the replacement template must resolve in the context
14// in which this command is invoked. Paths in the search pattern will then match the code if they
15// resolve to the same item, even if they're written differently. For example if we invoke the
16// command in the module `foo` with a pattern of `Bar`, then code in the parent module that refers
17// to `foo::Bar` will match.
18//
19// Paths in the replacement template will be rendered appropriately for the context in which the
20// replacement occurs. For example if our replacement template is `foo::Bar` and we match some
21// code in the `foo` module, we'll insert just `Bar`.
22//
23// Inherent method calls should generally be written in UFCS form. e.g. `foo::Bar::baz($s, $a)` will
24// match `$s.baz($a)`, provided the method call `baz` resolves to the method `foo::Bar::baz`. When a
25// placeholder is the receiver of a method call in the search pattern (e.g. `$s.foo()`), but not in
26// the replacement template (e.g. `bar($s)`), then *, & and &mut will be added as needed to mirror
27// whatever autoderef and autoref was happening implicitly in the matched code.
28//
29// The scope of the search / replace will be restricted to the current selection if any, otherwise
30// it will apply to the whole workspace.
31//
32// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
33//
34// Supported constraints:
35//
36// | Constraint    | Restricts placeholder |
37// |---------------|------------------------|
38// | kind(literal) | Is a literal (e.g. `42` or `"forty two"`) |
39// | not(a)        | Negates the constraint `a` |
40//
41// Available via the command `rust-analyzer.ssr`.
42//
43// ```rust
44// // Using structural search replace command [foo($a, $b) ==>> ($a).foo($b)]
45//
46// // BEFORE
47// String::from(foo(y + 5, z))
48//
49// // AFTER
50// String::from((y + 5).foo(z))
51// ```
52//
53// | Editor  | Action Name |
54// |---------|--------------|
55// | VS Code | **rust-analyzer: Structural Search Replace** |
56//
57// Also available as an assist, by writing a comment containing the structural
58// search and replace rule. You will only see the assist if the comment can
59// be parsed as a valid structural search and replace rule.
60//
61// ```rust
62// // Place the cursor on the line below to see the assist 💡.
63// // foo($a, $b) ==>> ($a).foo($b)
64// ```
65
66#![cfg_attr(feature = "in-rust-tree", feature(rustc_private))]
67
68#[cfg(feature = "in-rust-tree")]
69extern crate rustc_driver as _;
70
71mod fragments;
72mod from_comment;
73mod matching;
74mod nester;
75mod parsing;
76mod replacing;
77mod resolving;
78mod search;
79#[macro_use]
80mod errors;
81#[cfg(test)]
82mod tests;
83
84pub use crate::{errors::SsrError, from_comment::ssr_from_comment, matching::Match};
85
86use crate::{errors::bail, matching::MatchFailureReason};
87use hir::{FileRange, Semantics};
88use ide_db::symbol_index::LocalRoots;
89use ide_db::text_edit::TextEdit;
90use ide_db::{EditionedFileId, FileId, FxHashMap, RootDatabase, base_db::SourceDatabase};
91use resolving::ResolvedRule;
92use syntax::{AstNode, SyntaxNode, TextRange, ast};
93
94// A structured search replace rule. Create by calling `parse` on a str.
95#[derive(Debug)]
96pub struct SsrRule {
97    /// A structured pattern that we're searching for.
98    pattern: parsing::RawPattern,
99    /// What we'll replace it with.
100    template: parsing::RawPattern,
101    parsed_rules: Vec<parsing::ParsedRule>,
102}
103
104#[derive(Debug)]
105pub struct SsrPattern {
106    parsed_rules: Vec<parsing::ParsedRule>,
107}
108
109#[derive(Debug, Default)]
110pub struct SsrMatches {
111    pub matches: Vec<Match>,
112}
113
114/// Searches a crate for pattern matches and possibly replaces them with something else.
115pub struct MatchFinder<'db> {
116    /// Our source of information about the user's code.
117    sema: Semantics<'db, ide_db::RootDatabase>,
118    rules: Vec<ResolvedRule<'db>>,
119    resolution_scope: resolving::ResolutionScope<'db>,
120    restrict_ranges: Vec<ide_db::FileRange>,
121}
122
123impl<'db> MatchFinder<'db> {
124    /// Constructs a new instance where names will be looked up as if they appeared at
125    /// `lookup_context`.
126    pub fn in_context(
127        db: &'db RootDatabase,
128        lookup_context: ide_db::FilePosition,
129        mut restrict_ranges: Vec<ide_db::FileRange>,
130    ) -> Result<MatchFinder<'db>, SsrError> {
131        restrict_ranges.retain(|range| !range.range.is_empty());
132        let sema = Semantics::new(db);
133        let file_id = sema.attach_first_edition(lookup_context.file_id);
134        let resolution_scope = resolving::ResolutionScope::new(
135            &sema,
136            hir::FilePosition { file_id, offset: lookup_context.offset },
137        )
138        .ok_or_else(|| SsrError("no resolution scope for file".into()))?;
139        Ok(MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges })
140    }
141
142    /// Constructs an instance using the start of the first file in `db` as the lookup context.
143    pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
144        if let Some(first_file_id) = LocalRoots::get(db)
145            .roots(db)
146            .iter()
147            .next()
148            .and_then(|root| db.source_root(*root).source_root(db).iter().next())
149        {
150            MatchFinder::in_context(
151                db,
152                ide_db::FilePosition { file_id: first_file_id, offset: 0.into() },
153                vec![],
154            )
155        } else {
156            bail!("No files to search");
157        }
158    }
159
160    /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
161    /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
162    /// match to it.
163    pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
164        for parsed_rule in rule.parsed_rules {
165            self.rules.push(ResolvedRule::new(
166                parsed_rule,
167                &self.resolution_scope,
168                self.rules.len(),
169            )?);
170        }
171        Ok(())
172    }
173
174    /// Finds matches for all added rules and returns edits for all found matches.
175    pub fn edits(&self) -> FxHashMap<FileId, TextEdit> {
176        let mut matches_by_file = FxHashMap::default();
177        for m in self.matches().matches {
178            matches_by_file
179                .entry(m.range.file_id.file_id(self.sema.db))
180                .or_insert_with(SsrMatches::default)
181                .matches
182                .push(m);
183        }
184        matches_by_file
185            .into_iter()
186            .map(|(file_id, matches)| {
187                (
188                    file_id,
189                    replacing::matches_to_edit(
190                        self.sema.db,
191                        &matches,
192                        self.sema.db.file_text(file_id).text(self.sema.db),
193                        &self.rules,
194                    ),
195                )
196            })
197            .collect()
198    }
199
200    /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
201    /// intend to do replacement, use `add_rule` instead.
202    pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
203        for parsed_rule in pattern.parsed_rules {
204            self.rules.push(ResolvedRule::new(
205                parsed_rule,
206                &self.resolution_scope,
207                self.rules.len(),
208            )?);
209        }
210        Ok(())
211    }
212
213    /// Returns matches for all added rules.
214    pub fn matches(&self) -> SsrMatches {
215        let mut matches = Vec::new();
216        let mut usage_cache = search::UsageCache::default();
217        for rule in &self.rules {
218            self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
219        }
220        nester::nest_and_remove_collisions(matches, &self.sema)
221    }
222
223    /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
224    /// them, while recording reasons why they don't match. This API is useful for command
225    /// line-based debugging where providing a range is difficult.
226    pub fn debug_where_text_equal(
227        &self,
228        file_id: EditionedFileId,
229        snippet: &str,
230    ) -> Vec<MatchDebugInfo> {
231        let file = self.sema.parse(file_id);
232        let mut res = Vec::new();
233        let file_text = self.sema.db.file_text(file_id.file_id(self.sema.db)).text(self.sema.db);
234        let mut remaining_text = &**file_text;
235        let mut base = 0;
236        let len = snippet.len() as u32;
237        while let Some(offset) = remaining_text.find(snippet) {
238            let start = base + offset as u32;
239            let end = start + len;
240            self.output_debug_for_nodes_at_range(
241                file.syntax(),
242                FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
243                &None,
244                &mut res,
245            );
246            remaining_text = &remaining_text[offset + snippet.len()..];
247            base = end;
248        }
249        res
250    }
251
252    fn output_debug_for_nodes_at_range(
253        &self,
254        node: &SyntaxNode,
255        range: FileRange,
256        restrict_range: &Option<FileRange>,
257        out: &mut Vec<MatchDebugInfo>,
258    ) {
259        for node in node.children() {
260            let node_range = self.sema.original_range(&node);
261            if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
262            {
263                continue;
264            }
265            if node_range.range == range.range {
266                for rule in &self.rules {
267                    // For now we ignore rules that have a different kind than our node, otherwise
268                    // we get lots of noise. If at some point we add support for restricting rules
269                    // to a particular kind of thing (e.g. only match type references), then we can
270                    // relax this. We special-case expressions, since function calls can match
271                    // method calls.
272                    if rule.pattern.node.kind() != node.kind()
273                        && !(ast::Expr::can_cast(rule.pattern.node.kind())
274                            && ast::Expr::can_cast(node.kind()))
275                    {
276                        continue;
277                    }
278                    out.push(MatchDebugInfo {
279                        matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
280                            .map_err(|e| MatchFailureReason {
281                                reason: e.reason.unwrap_or_else(|| {
282                                    "Match failed, but no reason was given".to_owned()
283                                }),
284                            }),
285                        pattern: rule.pattern.node.clone(),
286                        node: node.clone(),
287                    });
288                }
289            } else if let Some(macro_call) = ast::MacroCall::cast(node.clone())
290                && let Some(expanded) = self.sema.expand_macro_call(&macro_call)
291                && let Some(tt) = macro_call.token_tree()
292            {
293                self.output_debug_for_nodes_at_range(
294                    &expanded.value,
295                    range,
296                    &Some(self.sema.original_range(tt.syntax())),
297                    out,
298                );
299            }
300            self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
301        }
302    }
303}
304
305pub struct MatchDebugInfo {
306    node: SyntaxNode,
307    /// Our search pattern parsed as an expression or item, etc
308    pattern: SyntaxNode,
309    matched: Result<Match, MatchFailureReason>,
310}
311
312impl std::fmt::Debug for MatchDebugInfo {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        match &self.matched {
315            Ok(_) => writeln!(f, "Node matched")?,
316            Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?,
317        }
318        writeln!(
319            f,
320            "============ AST ===========\n\
321            {:#?}",
322            self.node
323        )?;
324        writeln!(f, "========= PATTERN ==========")?;
325        writeln!(f, "{:#?}", self.pattern)?;
326        writeln!(f, "============================")?;
327        Ok(())
328    }
329}
330
331impl SsrMatches {
332    /// Returns `self` with any nested matches removed and made into top-level matches.
333    pub fn flattened(self) -> SsrMatches {
334        let mut out = SsrMatches::default();
335        self.flatten_into(&mut out);
336        out
337    }
338
339    fn flatten_into(self, out: &mut SsrMatches) {
340        for mut m in self.matches {
341            for p in m.placeholder_values.values_mut() {
342                std::mem::take(&mut p.inner_matches).flatten_into(out);
343            }
344            out.matches.push(m);
345        }
346    }
347}
348
349impl Match {
350    pub fn matched_text(&self) -> String {
351        self.matched_node.text().to_string()
352    }
353}
354
355impl std::error::Error for SsrError {}
356
357#[cfg(test)]
358impl MatchDebugInfo {
359    pub fn match_failure_reason(&self) -> Option<&str> {
360        self.matched.as_ref().err().map(|r| r.reason.as_str())
361    }
362}