2020-06-17 06:53:51 +00:00
|
|
|
//! Structural Search Replace
|
|
|
|
//!
|
|
|
|
//! Allows searching the AST for code that matches one or more patterns and then replacing that code
|
|
|
|
//! based on a template.
|
|
|
|
|
|
|
|
mod matching;
|
2020-07-22 06:48:12 +00:00
|
|
|
mod nester;
|
2020-06-17 06:53:51 +00:00
|
|
|
mod parsing;
|
|
|
|
mod replacing;
|
2020-07-22 06:46:29 +00:00
|
|
|
mod resolving;
|
2020-07-22 06:31:32 +00:00
|
|
|
mod search;
|
2020-06-23 12:03:39 +00:00
|
|
|
#[macro_use]
|
|
|
|
mod errors;
|
2020-06-17 06:53:51 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests;
|
|
|
|
|
2020-07-22 05:00:28 +00:00
|
|
|
use crate::errors::bail;
|
2020-06-23 12:03:39 +00:00
|
|
|
pub use crate::errors::SsrError;
|
2020-06-30 05:55:20 +00:00
|
|
|
pub use crate::matching::Match;
|
2020-07-03 02:57:17 +00:00
|
|
|
use crate::matching::MatchFailureReason;
|
2020-06-17 06:53:51 +00:00
|
|
|
use hir::Semantics;
|
2020-07-22 05:00:28 +00:00
|
|
|
use ra_db::{FileId, FilePosition, FileRange};
|
2020-07-22 06:23:43 +00:00
|
|
|
use ra_ide_db::source_change::SourceFileEdit;
|
2020-07-22 06:46:29 +00:00
|
|
|
use resolving::ResolvedRule;
|
2020-07-22 06:23:43 +00:00
|
|
|
use rustc_hash::FxHashMap;
|
2020-08-12 16:26:51 +00:00
|
|
|
use syntax::{ast, AstNode, SyntaxNode, TextRange};
|
2020-06-17 06:53:51 +00:00
|
|
|
|
|
|
|
// 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.
|
2020-07-03 02:57:17 +00:00
|
|
|
pattern: parsing::RawPattern,
|
2020-06-17 06:53:51 +00:00
|
|
|
/// What we'll replace it with.
|
2020-07-22 09:15:19 +00:00
|
|
|
template: parsing::RawPattern,
|
2020-07-03 02:57:17 +00:00
|
|
|
parsed_rules: Vec<parsing::ParsedRule>,
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2020-06-30 05:55:20 +00:00
|
|
|
pub struct SsrPattern {
|
2020-07-03 02:57:17 +00:00
|
|
|
raw: parsing::RawPattern,
|
|
|
|
parsed_rules: Vec<parsing::ParsedRule>,
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
pub struct SsrMatches {
|
2020-06-30 05:55:20 +00:00
|
|
|
pub matches: Vec<Match>,
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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, ra_ide_db::RootDatabase>,
|
2020-07-22 06:46:29 +00:00
|
|
|
rules: Vec<ResolvedRule>,
|
2020-07-26 04:49:18 +00:00
|
|
|
resolution_scope: resolving::ResolutionScope<'db>,
|
2020-07-29 01:44:01 +00:00
|
|
|
restrict_ranges: Vec<FileRange>,
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'db> MatchFinder<'db> {
|
2020-07-22 05:00:28 +00:00
|
|
|
/// Constructs a new instance where names will be looked up as if they appeared at
|
|
|
|
/// `lookup_context`.
|
|
|
|
pub fn in_context(
|
|
|
|
db: &'db ra_ide_db::RootDatabase,
|
2020-07-22 06:46:29 +00:00
|
|
|
lookup_context: FilePosition,
|
2020-07-29 01:44:01 +00:00
|
|
|
mut restrict_ranges: Vec<FileRange>,
|
2020-07-22 05:00:28 +00:00
|
|
|
) -> MatchFinder<'db> {
|
2020-07-29 01:44:01 +00:00
|
|
|
restrict_ranges.retain(|range| !range.range.is_empty());
|
2020-07-22 06:46:29 +00:00
|
|
|
let sema = Semantics::new(db);
|
2020-07-26 04:49:18 +00:00
|
|
|
let resolution_scope = resolving::ResolutionScope::new(&sema, lookup_context);
|
2020-08-01 07:41:42 +00:00
|
|
|
MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges }
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
2020-07-22 05:00:28 +00:00
|
|
|
/// Constructs an instance using the start of the first file in `db` as the lookup context.
|
|
|
|
pub fn at_first_file(db: &'db ra_ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
|
|
|
|
use ra_db::SourceDatabaseExt;
|
|
|
|
use ra_ide_db::symbol_index::SymbolsDatabase;
|
|
|
|
if let Some(first_file_id) = db
|
|
|
|
.local_roots()
|
|
|
|
.iter()
|
|
|
|
.next()
|
|
|
|
.and_then(|root| db.source_root(root.clone()).iter().next())
|
|
|
|
{
|
|
|
|
Ok(MatchFinder::in_context(
|
|
|
|
db,
|
|
|
|
FilePosition { file_id: first_file_id, offset: 0.into() },
|
2020-07-29 01:44:01 +00:00
|
|
|
vec![],
|
2020-07-22 05:00:28 +00:00
|
|
|
))
|
|
|
|
} else {
|
|
|
|
bail!("No files to search");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-03 02:57:17 +00:00
|
|
|
/// 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.
|
2020-07-22 06:46:29 +00:00
|
|
|
pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
|
|
|
|
for parsed_rule in rule.parsed_rules {
|
|
|
|
self.rules.push(ResolvedRule::new(
|
|
|
|
parsed_rule,
|
2020-07-26 04:49:18 +00:00
|
|
|
&self.resolution_scope,
|
2020-07-22 06:46:29 +00:00
|
|
|
self.rules.len(),
|
|
|
|
)?);
|
|
|
|
}
|
|
|
|
Ok(())
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
2020-07-22 06:23:43 +00:00
|
|
|
/// Finds matches for all added rules and returns edits for all found matches.
|
|
|
|
pub fn edits(&self) -> Vec<SourceFileEdit> {
|
|
|
|
use ra_db::SourceDatabaseExt;
|
|
|
|
let mut matches_by_file = FxHashMap::default();
|
|
|
|
for m in self.matches().matches {
|
|
|
|
matches_by_file
|
|
|
|
.entry(m.range.file_id)
|
|
|
|
.or_insert_with(|| SsrMatches::default())
|
|
|
|
.matches
|
|
|
|
.push(m);
|
|
|
|
}
|
|
|
|
let mut edits = vec![];
|
|
|
|
for (file_id, matches) in matches_by_file {
|
|
|
|
let edit =
|
|
|
|
replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules);
|
|
|
|
edits.push(SourceFileEdit { file_id, edit });
|
|
|
|
}
|
|
|
|
edits
|
|
|
|
}
|
|
|
|
|
2020-06-30 05:55:20 +00:00
|
|
|
/// 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.
|
2020-07-22 06:46:29 +00:00
|
|
|
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,
|
2020-07-26 04:49:18 +00:00
|
|
|
&self.resolution_scope,
|
2020-07-22 06:46:29 +00:00
|
|
|
self.rules.len(),
|
|
|
|
)?);
|
|
|
|
}
|
|
|
|
Ok(())
|
2020-06-30 05:55:20 +00:00
|
|
|
}
|
|
|
|
|
2020-07-22 06:23:43 +00:00
|
|
|
/// Returns matches for all added rules.
|
|
|
|
pub fn matches(&self) -> SsrMatches {
|
|
|
|
let mut matches = Vec::new();
|
2020-07-22 04:01:21 +00:00
|
|
|
let mut usage_cache = search::UsageCache::default();
|
2020-07-22 06:48:12 +00:00
|
|
|
for rule in &self.rules {
|
2020-07-22 04:01:21 +00:00
|
|
|
self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
|
2020-07-22 06:48:12 +00:00
|
|
|
}
|
|
|
|
nester::nest_and_remove_collisions(matches, &self.sema)
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
2020-06-30 05:55:20 +00:00
|
|
|
/// 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: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
|
|
|
|
use ra_db::SourceDatabaseExt;
|
|
|
|
let file = self.sema.parse(file_id);
|
|
|
|
let mut res = Vec::new();
|
|
|
|
let file_text = self.sema.db.file_text(file_id);
|
|
|
|
let mut remaining_text = file_text.as_str();
|
|
|
|
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 {
|
2020-07-03 02:57:17 +00:00
|
|
|
// 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
|
2020-07-24 10:53:48 +00:00
|
|
|
// 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()))
|
|
|
|
{
|
2020-07-03 02:57:17 +00:00
|
|
|
continue;
|
|
|
|
}
|
2020-06-30 05:55:20 +00:00
|
|
|
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()
|
|
|
|
}),
|
|
|
|
}),
|
2020-07-22 06:46:29 +00:00
|
|
|
pattern: rule.pattern.node.clone(),
|
2020-06-30 05:55:20 +00:00
|
|
|
node: node.clone(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if let Some(macro_call) = ast::MacroCall::cast(node.clone()) {
|
|
|
|
if let Some(expanded) = self.sema.expand(¯o_call) {
|
|
|
|
if let Some(tt) = macro_call.token_tree() {
|
|
|
|
self.output_debug_for_nodes_at_range(
|
|
|
|
&expanded,
|
|
|
|
range,
|
|
|
|
&Some(self.sema.original_range(tt.syntax())),
|
|
|
|
out,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-03 03:15:00 +00:00
|
|
|
self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
|
2020-06-30 05:55:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct MatchDebugInfo {
|
|
|
|
node: SyntaxNode,
|
2020-07-03 02:57:17 +00:00
|
|
|
/// Our search pattern parsed as an expression or item, etc
|
|
|
|
pattern: SyntaxNode,
|
2020-06-30 05:55:20 +00:00
|
|
|
matched: Result<Match, MatchFailureReason>,
|
2020-06-17 06:53:51 +00:00
|
|
|
}
|
|
|
|
|
2020-06-30 05:55:20 +00:00
|
|
|
impl std::fmt::Debug for MatchDebugInfo {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
2020-07-03 03:15:00 +00:00
|
|
|
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 ==========")?;
|
2020-07-03 02:57:17 +00:00
|
|
|
writeln!(f, "{:#?}", self.pattern)?;
|
2020-07-03 03:15:00 +00:00
|
|
|
writeln!(f, "============================")?;
|
2020-06-30 05:55:20 +00:00
|
|
|
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::replace(&mut p.inner_matches, SsrMatches::default()).flatten_into(out);
|
|
|
|
}
|
|
|
|
out.matches.push(m);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Match {
|
|
|
|
pub fn matched_text(&self) -> String {
|
|
|
|
self.matched_node.text().to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-17 06:53:51 +00:00
|
|
|
impl std::error::Error for SsrError {}
|
2020-06-30 05:55:20 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
impl MatchDebugInfo {
|
|
|
|
pub(crate) fn match_failure_reason(&self) -> Option<&str> {
|
|
|
|
self.matched.as_ref().err().map(|r| r.reason.as_str())
|
|
|
|
}
|
|
|
|
}
|