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