#![warn(rust_2018_idioms, unused_lifetimes)]
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::Semantics;
use ide_db::base_db::{FileId, FilePosition, FileRange};
use nohash_hasher::IntMap;
use resolving::ResolvedRule;
use syntax::{ast, AstNode, SyntaxNode, TextRange};
use text_edit::TextEdit;
#[derive(Debug)]
pub struct SsrRule {
pattern: parsing::RawPattern,
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>,
}
pub struct MatchFinder<'db> {
sema: Semantics<'db, ide_db::RootDatabase>,
rules: Vec<ResolvedRule>,
resolution_scope: resolving::ResolutionScope<'db>,
restrict_ranges: Vec<FileRange>,
}
impl<'db> MatchFinder<'db> {
pub fn in_context(
db: &'db ide_db::RootDatabase,
lookup_context: FilePosition,
mut restrict_ranges: Vec<FileRange>,
) -> Result<MatchFinder<'db>, SsrError> {
restrict_ranges.retain(|range| !range.range.is_empty());
let sema = Semantics::new(db);
let resolution_scope = resolving::ResolutionScope::new(&sema, lookup_context)
.ok_or_else(|| SsrError("no resolution scope for file".into()))?;
Ok(MatchFinder { sema, rules: Vec::new(), resolution_scope, restrict_ranges })
}
pub fn at_first_file(db: &'db ide_db::RootDatabase) -> Result<MatchFinder<'db>, SsrError> {
use ide_db::base_db::SourceDatabaseExt;
use ide_db::symbol_index::SymbolsDatabase;
if let Some(first_file_id) =
db.local_roots().iter().next().and_then(|root| db.source_root(*root).iter().next())
{
MatchFinder::in_context(
db,
FilePosition { file_id: first_file_id, offset: 0.into() },
vec![],
)
} else {
bail!("No files to search");
}
}
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(())
}
pub fn edits(&self) -> IntMap<FileId, TextEdit> {
use ide_db::base_db::SourceDatabaseExt;
let mut matches_by_file = IntMap::default();
for m in self.matches().matches {
matches_by_file
.entry(m.range.file_id)
.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),
&self.rules,
),
)
})
.collect()
}
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(())
}
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)
}
pub fn debug_where_text_equal(&self, file_id: FileId, snippet: &str) -> Vec<MatchDebugInfo> {
use ide_db::base_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;
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 {
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(¯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,
);
}
}
}
self.output_debug_for_nodes_at_range(&node, range, restrict_range, out);
}
}
}
pub struct MatchDebugInfo {
node: SyntaxNode,
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 {
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(crate) fn match_failure_reason(&self) -> Option<&str> {
self.matched.as_ref().err().map(|r| r.reason.as_str())
}
}