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