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