mirror of
				https://github.com/rust-lang/rust-analyzer.git
				synced 2025-10-30 03:27:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			360 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			360 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Structural Search Replace
 | |
| //!
 | |
| //! Allows searching the AST for code that matches one or more patterns and then replacing that code
 | |
| //! based on a template.
 | |
| 
 | |
| // Feature: Structural Search and Replace
 | |
| //
 | |
| // Search and replace with named wildcards that will match any expression, type, path, pattern or item.
 | |
| // The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`.
 | |
| // A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement.
 | |
| // Within a macro call, a placeholder will match up until whatever token follows the placeholder.
 | |
| //
 | |
| // All paths in both the search pattern and the replacement template must resolve in the context
 | |
| // in which this command is invoked. Paths in the search pattern will then match the code if they
 | |
| // resolve to the same item, even if they're written differently. For example if we invoke the
 | |
| // command in the module `foo` with a pattern of `Bar`, then code in the parent module that refers
 | |
| // to `foo::Bar` will match.
 | |
| //
 | |
| // Paths in the replacement template will be rendered appropriately for the context in which the
 | |
| // replacement occurs. For example if our replacement template is `foo::Bar` and we match some
 | |
| // code in the `foo` module, we'll insert just `Bar`.
 | |
| //
 | |
| // Inherent method calls should generally be written in UFCS form. e.g. `foo::Bar::baz($s, $a)` will
 | |
| // match `$s.baz($a)`, provided the method call `baz` resolves to the method `foo::Bar::baz`. When a
 | |
| // placeholder is the receiver of a method call in the search pattern (e.g. `$s.foo()`), but not in
 | |
| // the replacement template (e.g. `bar($s)`), then *, & and &mut will be added as needed to mirror
 | |
| // whatever autoderef and autoref was happening implicitly in the matched code.
 | |
| //
 | |
| // The scope of the search / replace will be restricted to the current selection if any, otherwise
 | |
| // it will apply to the whole workspace.
 | |
| //
 | |
| // Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
 | |
| //
 | |
| // Supported constraints:
 | |
| //
 | |
| // | Constraint    | Restricts placeholder |
 | |
| // |---------------|------------------------|
 | |
| // | kind(literal) | Is a literal (e.g. `42` or `"forty two"`) |
 | |
| // | not(a)        | Negates the constraint `a` |
 | |
| //
 | |
| // Available via the command `rust-analyzer.ssr`.
 | |
| //
 | |
| // ```rust
 | |
| // // Using structural search replace command [foo($a, $b) ==>> ($a).foo($b)]
 | |
| //
 | |
| // // BEFORE
 | |
| // String::from(foo(y + 5, z))
 | |
| //
 | |
| // // AFTER
 | |
| // String::from((y + 5).foo(z))
 | |
| // ```
 | |
| //
 | |
| // | Editor  | Action Name |
 | |
| // |---------|--------------|
 | |
| // | VS Code | **rust-analyzer: Structural Search Replace** |
 | |
| //
 | |
| // Also available as an assist, by writing a comment containing the structural
 | |
| // search and replace rule. You will only see the assist if the comment can
 | |
| // be parsed as a valid structural search and replace rule.
 | |
| //
 | |
| // ```rust
 | |
| // // Place the cursor on the line below to see the assist 💡.
 | |
| // // foo($a, $b) ==>> ($a).foo($b)
 | |
| // ```
 | |
| 
 | |
| mod fragments;
 | |
| mod from_comment;
 | |
| mod matching;
 | |
| mod nester;
 | |
| mod parsing;
 | |
| mod replacing;
 | |
| mod resolving;
 | |
| mod search;
 | |
| #[macro_use]
 | |
| mod errors;
 | |
| #[cfg(test)]
 | |
| mod tests;
 | |
| 
 | |
| pub use crate::{errors::SsrError, from_comment::ssr_from_comment, matching::Match};
 | |
| 
 | |
| use crate::{errors::bail, matching::MatchFailureReason};
 | |
| use hir::{FileRange, Semantics};
 | |
| use ide_db::symbol_index::SymbolsDatabase;
 | |
| use ide_db::text_edit::TextEdit;
 | |
| use ide_db::{EditionedFileId, FileId, FxHashMap, RootDatabase, base_db::SourceDatabase};
 | |
| use resolving::ResolvedRule;
 | |
| use syntax::{AstNode, SyntaxNode, TextRange, ast};
 | |
| 
 | |
| // A structured search replace rule. Create by calling `parse` on a str.
 | |
| #[derive(Debug)]
 | |
| pub struct SsrRule {
 | |
|     /// A structured pattern that we're searching for.
 | |
|     pattern: parsing::RawPattern,
 | |
|     /// What we'll replace it with.
 | |
|     template: parsing::RawPattern,
 | |
|     parsed_rules: Vec<parsing::ParsedRule>,
 | |
| }
 | |
| 
 | |
| #[derive(Debug)]
 | |
| pub struct SsrPattern {
 | |
|     parsed_rules: Vec<parsing::ParsedRule>,
 | |
| }
 | |
| 
 | |
| #[derive(Debug, Default)]
 | |
| pub struct SsrMatches {
 | |
|     pub matches: Vec<Match>,
 | |
| }
 | |
| 
 | |
| /// Searches a crate for pattern matches and possibly replaces them with something else.
 | |
| pub struct MatchFinder<'db> {
 | |
|     /// Our source of information about the user's code.
 | |
|     sema: Semantics<'db, ide_db::RootDatabase>,
 | |
|     rules: Vec<ResolvedRule<'db>>,
 | |
|     resolution_scope: resolving::ResolutionScope<'db>,
 | |
|     restrict_ranges: Vec<ide_db::FileRange>,
 | |
| }
 | |
| 
 | |
| impl<'db> MatchFinder<'db> {
 | |
|     /// Constructs a new instance where names will be looked up as if they appeared at
 | |
|     /// `lookup_context`.
 | |
|     pub fn in_context(
 | |
|         db: &'db RootDatabase,
 | |
|         lookup_context: ide_db::FilePosition,
 | |
|         mut restrict_ranges: Vec<ide_db::FileRange>,
 | |
|     ) -> Result<MatchFinder<'db>, SsrError> {
 | |
|         restrict_ranges.retain(|range| !range.range.is_empty());
 | |
|         let sema = Semantics::new(db);
 | |
|         let file_id = sema
 | |
|             .attach_first_edition(lookup_context.file_id)
 | |
|             .unwrap_or_else(|| EditionedFileId::current_edition(db, lookup_context.file_id));
 | |
|         let resolution_scope = resolving::ResolutionScope::new(
 | |
|             &sema,
 | |
|             hir::FilePosition { file_id, offset: lookup_context.offset },
 | |
|         )
 | |
|         .ok_or_else(|| SsrError("no resolution scope for file".into()))?;
 | |
|         Ok(MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges })
 | |
|     }
 | |
| 
 | |
|     /// Constructs an instance using the start of the first file in `db` as the lookup context.
 | |
|     pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
 | |
|         if let Some(first_file_id) = db
 | |
|             .local_roots()
 | |
|             .iter()
 | |
|             .next()
 | |
|             .and_then(|root| db.source_root(*root).source_root(db).iter().next())
 | |
|         {
 | |
|             MatchFinder::in_context(
 | |
|                 db,
 | |
|                 ide_db::FilePosition { file_id: first_file_id, offset: 0.into() },
 | |
|                 vec![],
 | |
|             )
 | |
|         } else {
 | |
|             bail!("No files to search");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
 | |
|     /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
 | |
|     /// match to it.
 | |
|     pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
 | |
|         for parsed_rule in rule.parsed_rules {
 | |
|             self.rules.push(ResolvedRule::new(
 | |
|                 parsed_rule,
 | |
|                 &self.resolution_scope,
 | |
|                 self.rules.len(),
 | |
|             )?);
 | |
|         }
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// Finds matches for all added rules and returns edits for all found matches.
 | |
|     pub fn edits(&self) -> FxHashMap<FileId, TextEdit> {
 | |
|         let mut matches_by_file = FxHashMap::default();
 | |
|         for m in self.matches().matches {
 | |
|             matches_by_file
 | |
|                 .entry(m.range.file_id.file_id(self.sema.db))
 | |
|                 .or_insert_with(SsrMatches::default)
 | |
|                 .matches
 | |
|                 .push(m);
 | |
|         }
 | |
|         matches_by_file
 | |
|             .into_iter()
 | |
|             .map(|(file_id, matches)| {
 | |
|                 (
 | |
|                     file_id,
 | |
|                     replacing::matches_to_edit(
 | |
|                         self.sema.db,
 | |
|                         &matches,
 | |
|                         &self.sema.db.file_text(file_id).text(self.sema.db),
 | |
|                         &self.rules,
 | |
|                     ),
 | |
|                 )
 | |
|             })
 | |
|             .collect()
 | |
|     }
 | |
| 
 | |
|     /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
 | |
|     /// intend to do replacement, use `add_rule` instead.
 | |
|     pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
 | |
|         for parsed_rule in pattern.parsed_rules {
 | |
|             self.rules.push(ResolvedRule::new(
 | |
|                 parsed_rule,
 | |
|                 &self.resolution_scope,
 | |
|                 self.rules.len(),
 | |
|             )?);
 | |
|         }
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// Returns matches for all added rules.
 | |
|     pub fn matches(&self) -> SsrMatches {
 | |
|         let mut matches = Vec::new();
 | |
|         let mut usage_cache = search::UsageCache::default();
 | |
|         for rule in &self.rules {
 | |
|             self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
 | |
|         }
 | |
|         nester::nest_and_remove_collisions(matches, &self.sema)
 | |
|     }
 | |
| 
 | |
|     /// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match
 | |
|     /// them, while recording reasons why they don't match. This API is useful for command
 | |
|     /// line-based debugging where providing a range is difficult.
 | |
|     pub fn debug_where_text_equal(
 | |
|         &self,
 | |
|         file_id: EditionedFileId,
 | |
|         snippet: &str,
 | |
|     ) -> Vec<MatchDebugInfo> {
 | |
|         let file = self.sema.parse(file_id);
 | |
|         let mut res = Vec::new();
 | |
|         let file_text = self.sema.db.file_text(file_id.file_id(self.sema.db)).text(self.sema.db);
 | |
|         let mut remaining_text = &*file_text;
 | |
|         let mut base = 0;
 | |
|         let len = snippet.len() as u32;
 | |
|         while let Some(offset) = remaining_text.find(snippet) {
 | |
|             let start = base + offset as u32;
 | |
|             let end = start + len;
 | |
|             self.output_debug_for_nodes_at_range(
 | |
|                 file.syntax(),
 | |
|                 FileRange { file_id, range: TextRange::new(start.into(), end.into()) },
 | |
|                 &None,
 | |
|                 &mut res,
 | |
|             );
 | |
|             remaining_text = &remaining_text[offset + snippet.len()..];
 | |
|             base = end;
 | |
|         }
 | |
|         res
 | |
|     }
 | |
| 
 | |
|     fn output_debug_for_nodes_at_range(
 | |
|         &self,
 | |
|         node: &SyntaxNode,
 | |
|         range: FileRange,
 | |
|         restrict_range: &Option<FileRange>,
 | |
|         out: &mut Vec<MatchDebugInfo>,
 | |
|     ) {
 | |
|         for node in node.children() {
 | |
|             let node_range = self.sema.original_range(&node);
 | |
|             if node_range.file_id != range.file_id || !node_range.range.contains_range(range.range)
 | |
|             {
 | |
|                 continue;
 | |
|             }
 | |
|             if node_range.range == range.range {
 | |
|                 for rule in &self.rules {
 | |
|                     // For now we ignore rules that have a different kind than our node, otherwise
 | |
|                     // we get lots of noise. If at some point we add support for restricting rules
 | |
|                     // to a particular kind of thing (e.g. only match type references), then we can
 | |
|                     // relax this. We special-case expressions, since function calls can match
 | |
|                     // method calls.
 | |
|                     if rule.pattern.node.kind() != node.kind()
 | |
|                         && !(ast::Expr::can_cast(rule.pattern.node.kind())
 | |
|                             && ast::Expr::can_cast(node.kind()))
 | |
|                     {
 | |
|                         continue;
 | |
|                     }
 | |
|                     out.push(MatchDebugInfo {
 | |
|                         matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
 | |
|                             .map_err(|e| MatchFailureReason {
 | |
|                                 reason: e.reason.unwrap_or_else(|| {
 | |
|                                     "Match failed, but no reason was given".to_owned()
 | |
|                                 }),
 | |
|                             }),
 | |
|                         pattern: rule.pattern.node.clone(),
 | |
|                         node: node.clone(),
 | |
|                     });
 | |
|                 }
 | |
|             } else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
 | |
|                 if let Some(expanded) = self.sema.expand_macro_call(¯o_call) {
 | |
|                     if let Some(tt) = macro_call.token_tree() {
 | |
|                         self.output_debug_for_nodes_at_range(
 | |
|                             &expanded.value,
 | |
|                             range,
 | |
|                             &Some(self.sema.original_range(tt.syntax())),
 | |
|                             out,
 | |
|                         );
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| pub struct MatchDebugInfo {
 | |
|     node: SyntaxNode,
 | |
|     /// Our search pattern parsed as an expression or item, etc
 | |
|     pattern: SyntaxNode,
 | |
|     matched: Result<Match, MatchFailureReason>,
 | |
| }
 | |
| 
 | |
| impl std::fmt::Debug for MatchDebugInfo {
 | |
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | |
|         match &self.matched {
 | |
|             Ok(_) => writeln!(f, "Node matched")?,
 | |
|             Err(reason) => writeln!(f, "Node failed to match because: {}", reason.reason)?,
 | |
|         }
 | |
|         writeln!(
 | |
|             f,
 | |
|             "============ AST ===========\n\
 | |
|             {:#?}",
 | |
|             self.node
 | |
|         )?;
 | |
|         writeln!(f, "========= PATTERN ==========")?;
 | |
|         writeln!(f, "{:#?}", self.pattern)?;
 | |
|         writeln!(f, "============================")?;
 | |
|         Ok(())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl SsrMatches {
 | |
|     /// Returns `self` with any nested matches removed and made into top-level matches.
 | |
|     pub fn flattened(self) -> SsrMatches {
 | |
|         let mut out = SsrMatches::default();
 | |
|         self.flatten_into(&mut out);
 | |
|         out
 | |
|     }
 | |
| 
 | |
|     fn flatten_into(self, out: &mut SsrMatches) {
 | |
|         for mut m in self.matches {
 | |
|             for p in m.placeholder_values.values_mut() {
 | |
|                 std::mem::take(&mut p.inner_matches).flatten_into(out);
 | |
|             }
 | |
|             out.matches.push(m);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Match {
 | |
|     pub fn matched_text(&self) -> String {
 | |
|         self.matched_node.text().to_string()
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl std::error::Error for SsrError {}
 | |
| 
 | |
| #[cfg(test)]
 | |
| impl MatchDebugInfo {
 | |
|     pub fn match_failure_reason(&self) -> Option<&str> {
 | |
|         self.matched.as_ref().err().map(|r| r.reason.as_str())
 | |
|     }
 | |
| }
 | 
