SSR: Use Definition::find_usages to speed up matching.

When the search pattern contains a path, this substantially speeds up finding matches, especially if the path references a private item.
This commit is contained in:
David Lattimore 2020-07-22 14:01:21 +10:00
parent 757f755c29
commit 63f500b0ee
5 changed files with 158 additions and 25 deletions

View file

@ -290,7 +290,12 @@ pub fn classify_name_ref(
let path = name_ref.syntax().ancestors().find_map(ast::Path::cast)?; let path = name_ref.syntax().ancestors().find_map(ast::Path::cast)?;
let resolved = sema.resolve_path(&path)?; let resolved = sema.resolve_path(&path)?;
let res = match resolved { Some(NameRefClass::Definition(resolved.into()))
}
impl From<PathResolution> for Definition {
fn from(path_resolution: PathResolution) -> Self {
match path_resolution {
PathResolution::Def(def) => Definition::ModuleDef(def), PathResolution::Def(def) => Definition::ModuleDef(def),
PathResolution::AssocItem(item) => { PathResolution::AssocItem(item) => {
let def = match item { let def = match item {
@ -304,6 +309,6 @@ pub fn classify_name_ref(
PathResolution::TypeParam(par) => Definition::TypeParam(par), PathResolution::TypeParam(par) => Definition::TypeParam(par),
PathResolution::Macro(def) => Definition::Macro(def), PathResolution::Macro(def) => Definition::Macro(def),
PathResolution::SelfType(impl_def) => Definition::SelfType(impl_def), PathResolution::SelfType(impl_def) => Definition::SelfType(impl_def),
}; }
Some(NameRefClass::Definition(res)) }
} }

View file

@ -60,6 +60,10 @@ impl SearchScope {
SearchScope::new(std::iter::once((file, None)).collect()) SearchScope::new(std::iter::once((file, None)).collect())
} }
pub fn files(files: &[FileId]) -> SearchScope {
SearchScope::new(files.iter().map(|f| (*f, None)).collect())
}
pub fn intersection(&self, other: &SearchScope) -> SearchScope { pub fn intersection(&self, other: &SearchScope) -> SearchScope {
let (mut small, mut large) = (&self.entries, &other.entries); let (mut small, mut large) = (&self.entries, &other.entries);
if small.len() > large.len() { if small.len() > large.len() {

View file

@ -151,8 +151,9 @@ impl<'db> MatchFinder<'db> {
/// Returns matches for all added rules. /// Returns matches for all added rules.
pub fn matches(&self) -> SsrMatches { pub fn matches(&self) -> SsrMatches {
let mut matches = Vec::new(); let mut matches = Vec::new();
let mut usage_cache = search::UsageCache::default();
for rule in &self.rules { for rule in &self.rules {
self.find_matches_for_rule(rule, &mut matches); self.find_matches_for_rule(rule, &mut usage_cache, &mut matches);
} }
nester::nest_and_remove_collisions(matches, &self.sema) nester::nest_and_remove_collisions(matches, &self.sema)
} }

View file

@ -22,6 +22,7 @@ pub(crate) struct ResolvedPattern {
pub(crate) struct ResolvedPath { pub(crate) struct ResolvedPath {
pub(crate) resolution: hir::PathResolution, pub(crate) resolution: hir::PathResolution,
pub(crate) depth: u32,
} }
impl ResolvedRule { impl ResolvedRule {
@ -62,7 +63,7 @@ struct Resolver<'a, 'db> {
impl Resolver<'_, '_> { impl Resolver<'_, '_> {
fn resolve_pattern_tree(&self, pattern: SyntaxNode) -> Result<ResolvedPattern, SsrError> { fn resolve_pattern_tree(&self, pattern: SyntaxNode) -> Result<ResolvedPattern, SsrError> {
let mut resolved_paths = FxHashMap::default(); let mut resolved_paths = FxHashMap::default();
self.resolve(pattern.clone(), &mut resolved_paths)?; self.resolve(pattern.clone(), 0, &mut resolved_paths)?;
Ok(ResolvedPattern { Ok(ResolvedPattern {
node: pattern, node: pattern,
resolved_paths, resolved_paths,
@ -73,6 +74,7 @@ impl Resolver<'_, '_> {
fn resolve( fn resolve(
&self, &self,
node: SyntaxNode, node: SyntaxNode,
depth: u32,
resolved_paths: &mut FxHashMap<SyntaxNode, ResolvedPath>, resolved_paths: &mut FxHashMap<SyntaxNode, ResolvedPath>,
) -> Result<(), SsrError> { ) -> Result<(), SsrError> {
use ra_syntax::ast::AstNode; use ra_syntax::ast::AstNode;
@ -86,12 +88,12 @@ impl Resolver<'_, '_> {
let resolution = self let resolution = self
.resolve_path(&path) .resolve_path(&path)
.ok_or_else(|| error!("Failed to resolve path `{}`", node.text()))?; .ok_or_else(|| error!("Failed to resolve path `{}`", node.text()))?;
resolved_paths.insert(node, ResolvedPath { resolution }); resolved_paths.insert(node, ResolvedPath { resolution, depth });
return Ok(()); return Ok(());
} }
} }
for node in node.children() { for node in node.children() {
self.resolve(node, resolved_paths)?; self.resolve(node, depth + 1, resolved_paths)?;
} }
Ok(()) Ok(())
} }

View file

@ -1,17 +1,106 @@
//! Searching for matches. //! Searching for matches.
use crate::{matching, resolving::ResolvedRule, Match, MatchFinder}; use crate::{
matching,
resolving::{ResolvedPath, ResolvedPattern, ResolvedRule},
Match, MatchFinder,
};
use ra_db::FileRange; use ra_db::FileRange;
use ra_ide_db::{
defs::Definition,
search::{Reference, SearchScope},
};
use ra_syntax::{ast, AstNode, SyntaxNode}; use ra_syntax::{ast, AstNode, SyntaxNode};
/// A cache for the results of find_usages. This is for when we have multiple patterns that have the
/// same path. e.g. if the pattern was `foo::Bar` that can parse as a path, an expression, a type
/// and as a pattern. In each, the usages of `foo::Bar` are the same and we'd like to avoid finding
/// them more than once.
#[derive(Default)]
pub(crate) struct UsageCache {
usages: Vec<(Definition, Vec<Reference>)>,
}
impl<'db> MatchFinder<'db> { impl<'db> MatchFinder<'db> {
/// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make /// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make
/// replacement impossible, so further processing is required in order to properly nest matches /// replacement impossible, so further processing is required in order to properly nest matches
/// and remove overlapping matches. This is done in the `nesting` module. /// and remove overlapping matches. This is done in the `nesting` module.
pub(crate) fn find_matches_for_rule(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) { pub(crate) fn find_matches_for_rule(
// FIXME: Use resolved paths in the pattern to find places to search instead of always &self,
// scanning every node. rule: &ResolvedRule,
usage_cache: &mut UsageCache,
matches_out: &mut Vec<Match>,
) {
if pick_path_for_usages(&rule.pattern).is_none() {
self.slow_scan(rule, matches_out); self.slow_scan(rule, matches_out);
return;
}
self.find_matches_for_pattern_tree(rule, &rule.pattern, usage_cache, matches_out);
}
fn find_matches_for_pattern_tree(
&self,
rule: &ResolvedRule,
pattern: &ResolvedPattern,
usage_cache: &mut UsageCache,
matches_out: &mut Vec<Match>,
) {
if let Some(first_path) = pick_path_for_usages(pattern) {
let definition: Definition = first_path.resolution.clone().into();
for reference in self.find_usages(usage_cache, definition) {
let file = self.sema.parse(reference.file_range.file_id);
if let Some(path) = self.sema.find_node_at_offset_with_descend::<ast::Path>(
file.syntax(),
reference.file_range.range.start(),
) {
if let Some(node_to_match) = self
.sema
.ancestors_with_macros(path.syntax().clone())
.skip(first_path.depth as usize)
.next()
{
if let Ok(m) =
matching::get_match(false, rule, &node_to_match, &None, &self.sema)
{
matches_out.push(m);
}
}
}
}
}
}
fn find_usages<'a>(
&self,
usage_cache: &'a mut UsageCache,
definition: Definition,
) -> &'a [Reference] {
// Logically if a lookup succeeds we should just return it. Unfortunately returning it would
// extend the lifetime of the borrow, then we wouldn't be able to do the insertion on a
// cache miss. This is a limitation of NLL and is fixed with Polonius. For now we do two
// lookups in the case of a cache hit.
if usage_cache.find(&definition).is_none() {
let usages = definition.find_usages(&self.sema, Some(self.search_scope()));
usage_cache.usages.push((definition, usages));
return &usage_cache.usages.last().unwrap().1;
}
usage_cache.find(&definition).unwrap()
}
/// Returns the scope within which we want to search. We don't want un unrestricted search
/// scope, since we don't want to find references in external dependencies.
fn search_scope(&self) -> SearchScope {
// FIXME: We should ideally have a test that checks that we edit local roots and not library
// roots. This probably would require some changes to fixtures, since currently everything
// seems to get put into a single source root.
use ra_db::SourceDatabaseExt;
use ra_ide_db::symbol_index::SymbolsDatabase;
let mut files = Vec::new();
for &root in self.sema.db.local_roots().iter() {
let sr = self.sema.db.source_root(root);
files.extend(sr.iter());
}
SearchScope::files(&files)
} }
fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) { fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec<Match>) {
@ -59,3 +148,35 @@ impl<'db> MatchFinder<'db> {
} }
} }
} }
impl UsageCache {
fn find(&mut self, definition: &Definition) -> Option<&[Reference]> {
// We expect a very small number of cache entries (generally 1), so a linear scan should be
// fast enough and avoids the need to implement Hash for Definition.
for (d, refs) in &self.usages {
if d == definition {
return Some(refs);
}
}
None
}
}
/// Returns a path that's suitable for path resolution. We exclude builtin types, since they aren't
/// something that we can find references to. We then somewhat arbitrarily pick the path that is the
/// longest as this is hopefully more likely to be less common, making it faster to find.
fn pick_path_for_usages(pattern: &ResolvedPattern) -> Option<&ResolvedPath> {
// FIXME: Take the scope of the resolved path into account. e.g. if there are any paths that are
// private to the current module, then we definitely would want to pick them over say a path
// from std. Possibly we should go further than this and intersect the search scopes for all
// resolved paths then search only in that scope.
pattern
.resolved_paths
.iter()
.filter(|(_, p)| {
!matches!(p.resolution, hir::PathResolution::Def(hir::ModuleDef::BuiltinType(_)))
})
.map(|(node, resolved)| (node.text().len(), resolved))
.max_by(|(a, _), (b, _)| a.cmp(b))
.map(|(_, resolved)| resolved)
}