fix: Fix path qualified auto-importing completions not working with re-exports

Prior to this commit we used to generate import paths, then zipped them with the existing qualifier to check if they agree on the path to import.
This is brittle when re-exports come into play causing items to have multiple applicable paths that refer to them.
This commit instead rewrites this logic by generating the import path for the qualifier, verifying that the rest of the qualifier resolves and then doing a final lookup on that resolution result for the final segment instead.
This commit is contained in:
Lukas Wirth 2024-12-16 13:17:59 +01:00
parent 65c8d1242b
commit ada5f2059c
9 changed files with 224 additions and 137 deletions

View file

@ -3105,10 +3105,10 @@ impl From<ModuleDef> for ItemInNs {
}
impl ItemInNs {
pub fn as_module_def(self) -> Option<ModuleDef> {
pub fn into_module_def(self) -> ModuleDef {
match self {
ItemInNs::Types(id) | ItemInNs::Values(id) => Some(id),
ItemInNs::Macros(_) => None,
ItemInNs::Types(id) | ItemInNs::Values(id) => id,
ItemInNs::Macros(id) => ModuleDef::Macro(id),
}
}

View file

@ -39,8 +39,8 @@ use stdx::TupleExt;
use syntax::{
algo::skip_trivia_token,
ast::{self, HasAttrs as _, HasGenericParams},
AstNode, AstToken, Direction, SyntaxKind, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange,
TextSize,
AstNode, AstToken, Direction, SmolStr, SyntaxKind, SyntaxNode, SyntaxNodePtr, SyntaxToken,
TextRange, TextSize,
};
use triomphe::Arc;
@ -1540,6 +1540,21 @@ impl<'db> SemanticsImpl<'db> {
Some(items.iter_items().map(|(item, _)| item.into()))
}
pub fn resolve_mod_path_relative(
&self,
to: Module,
segments: impl IntoIterator<Item = SmolStr>,
) -> Option<impl Iterator<Item = ItemInNs>> {
let items = to.id.resolver(self.db.upcast()).resolve_module_path_in_items(
self.db.upcast(),
&ModPath::from_segments(
hir_def::path::PathKind::Plain,
segments.into_iter().map(|it| Name::new(&it, SyntaxContextId::ROOT)),
),
);
Some(items.iter_items().map(|(item, _)| item.into()))
}
fn resolve_variant(&self, record_lit: ast::RecordExpr) -> Option<VariantId> {
self.analyze(record_lit.syntax())?.resolve_variant(self.db, record_lit)
}

View file

@ -86,7 +86,7 @@ fn item_for_path_search(db: &dyn HirDatabase, item: ItemInNs) -> Option<ItemInNs
}
fn item_as_assoc(db: &dyn HirDatabase, item: ItemInNs) -> Option<AssocItem> {
item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db))
item.into_module_def().as_assoc_item(db)
}
#[cfg(test)]

View file

@ -51,7 +51,7 @@ pub(crate) fn qualify_path(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
let candidate = import_assets.import_candidate();
let qualify_candidate = match syntax_under_caret.clone() {
NodeOrToken::Node(syntax_under_caret) => match candidate {
ImportCandidate::Path(candidate) if candidate.qualifier.is_some() => {
ImportCandidate::Path(candidate) if !candidate.qualifier.is_empty() => {
cov_mark::hit!(qualify_path_qualifier_start);
let path = ast::Path::cast(syntax_under_caret)?;
let (prev_segment, segment) = (path.qualifier()?.segment()?, path.segment()?);
@ -219,11 +219,9 @@ fn find_trait_method(
}
fn item_as_trait(db: &RootDatabase, item: hir::ItemInNs) -> Option<hir::Trait> {
let item_module_def = item.as_module_def()?;
match item_module_def {
match item.into_module_def() {
hir::ModuleDef::Trait(trait_) => Some(trait_),
_ => item_module_def.as_assoc_item(db)?.container_trait(db),
item_module_def => item_module_def.as_assoc_item(db)?.container_trait(db),
}
}
@ -247,7 +245,7 @@ fn label(
let import_path = &import.import_path;
match candidate {
ImportCandidate::Path(candidate) if candidate.qualifier.is_none() => {
ImportCandidate::Path(candidate) if candidate.qualifier.is_empty() => {
format!("Qualify as `{}`", import_path.display(db, edition))
}
_ => format!("Qualify with `{}`", import_path.display(db, edition)),

View file

@ -78,7 +78,7 @@ pub(crate) fn replace_derive_with_manual_impl(
NameToImport::exact_case_sensitive(path.segments().last()?.to_string()),
items_locator::AssocSearchMode::Exclude,
)
.filter_map(|item| match item.as_module_def()? {
.filter_map(|item| match item.into_module_def() {
ModuleDef::Trait(trait_) => Some(trait_),
_ => None,
})

View file

@ -198,7 +198,7 @@ fn wrapper_alias(
);
ctx.sema.resolve_mod_path(ret_type.syntax(), &wrapper_path).and_then(|def| {
def.filter_map(|def| match def.as_module_def()? {
def.filter_map(|def| match def.into_module_def() {
hir::ModuleDef::TypeAlias(alias) => {
let enum_ty = alias.ty(ctx.db()).as_adt()?.as_enum()?;
(&enum_ty == core_wrapper).then_some(alias)

View file

@ -1720,3 +1720,45 @@ fn function() {
"#]],
);
}
#[test]
fn intrinsics() {
check(
r#"
//- /core.rs crate:core
pub mod intrinsics {
extern "rust-intrinsic" {
pub fn transmute<Src, Dst>(src: Src) -> Dst;
}
}
pub mod mem {
pub use crate::intrinsics::transmute;
}
//- /main.rs crate:main deps:core
fn function() {
transmute$0
}
"#,
expect![[r#"
fn transmute() (use core::mem::transmute) unsafe fn(Src) -> Dst
"#]],
);
check(
r#"
//- /core.rs crate:core
pub mod intrinsics {
extern "rust-intrinsic" {
pub fn transmute<Src, Dst>(src: Src) -> Dst;
}
}
pub mod mem {
pub use crate::intrinsics::transmute;
}
//- /main.rs crate:main deps:core
fn function() {
mem::transmute$0
}
"#,
expect![""],
);
}

View file

@ -2,10 +2,10 @@
use hir::{
db::HirDatabase, AsAssocItem, AssocItem, AssocItemContainer, Crate, HasCrate, ImportPathConfig,
ItemInNs, ModPath, Module, ModuleDef, Name, PathResolution, PrefixKind, ScopeDef, Semantics,
ItemInNs, ModPath, Module, ModuleDef, PathResolution, PrefixKind, ScopeDef, Semantics,
SemanticsScope, Trait, TyFingerprint, Type,
};
use itertools::{EitherOrBoth, Itertools};
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use syntax::{
ast::{self, make, HasName},
@ -13,7 +13,6 @@ use syntax::{
};
use crate::{
helpers::item_name,
items_locator::{self, AssocSearchMode, DEFAULT_QUERY_SEARCH_LIMIT},
FxIndexSet, RootDatabase,
};
@ -52,7 +51,7 @@ pub struct TraitImportCandidate {
#[derive(Debug)]
pub struct PathImportCandidate {
/// Optional qualifier before name.
pub qualifier: Option<Vec<SmolStr>>,
pub qualifier: Vec<SmolStr>,
/// The name the item (struct, trait, enum, etc.) should have.
pub name: NameToImport,
}
@ -264,7 +263,6 @@ impl ImportAssets {
Some(it) => it,
None => return <FxIndexSet<_>>::default().into_iter(),
};
let krate = self.module_with_candidate.krate();
let scope_definitions = self.scope_definitions(sema);
let mod_path = |item| {
@ -279,11 +277,14 @@ impl ImportAssets {
};
match &self.import_candidate {
ImportCandidate::Path(path_candidate) => {
path_applicable_imports(sema, krate, path_candidate, mod_path, |item_to_import| {
!scope_definitions.contains(&ScopeDef::from(item_to_import))
})
}
ImportCandidate::Path(path_candidate) => path_applicable_imports(
sema,
&scope,
krate,
path_candidate,
mod_path,
|item_to_import| !scope_definitions.contains(&ScopeDef::from(item_to_import)),
),
ImportCandidate::TraitAssocItem(trait_candidate)
| ImportCandidate::TraitMethod(trait_candidate) => trait_applicable_items(
sema,
@ -315,6 +316,7 @@ impl ImportAssets {
fn path_applicable_imports(
sema: &Semantics<'_, RootDatabase>,
scope: &SemanticsScope<'_>,
current_crate: Crate,
path_candidate: &PathImportCandidate,
mod_path: impl Fn(ItemInNs) -> Option<ModPath> + Copy,
@ -322,8 +324,8 @@ fn path_applicable_imports(
) -> FxIndexSet<LocatedImport> {
let _p = tracing::info_span!("ImportAssets::path_applicable_imports").entered();
match &path_candidate.qualifier {
None => {
match &*path_candidate.qualifier {
[] => {
items_locator::items_with_name(
sema,
current_crate,
@ -348,89 +350,107 @@ fn path_applicable_imports(
.take(DEFAULT_QUERY_SEARCH_LIMIT.inner())
.collect()
}
Some(qualifier) => items_locator::items_with_name(
[first_qsegment, qualifier_rest @ ..] => items_locator::items_with_name(
sema,
current_crate,
path_candidate.name.clone(),
AssocSearchMode::Include,
NameToImport::Exact(first_qsegment.to_string(), true),
AssocSearchMode::Exclude,
)
.filter_map(|item| import_for_item(sema.db, mod_path, qualifier, item, scope_filter))
.filter_map(|item| {
import_for_item(
sema,
scope,
mod_path,
&path_candidate.name,
item,
qualifier_rest,
scope_filter,
)
})
.take(DEFAULT_QUERY_SEARCH_LIMIT.inner())
.collect(),
}
}
fn import_for_item(
db: &RootDatabase,
sema: &Semantics<'_, RootDatabase>,
scope: &SemanticsScope<'_>,
mod_path: impl Fn(ItemInNs) -> Option<ModPath>,
candidate: &NameToImport,
resolved_qualifier: ItemInNs,
unresolved_qualifier: &[SmolStr],
original_item: ItemInNs,
scope_filter: impl Fn(ItemInNs) -> bool,
) -> Option<LocatedImport> {
let _p = tracing::info_span!("ImportAssets::import_for_item").entered();
let [first_segment, ..] = unresolved_qualifier else { return None };
let item_as_assoc = item_as_assoc(db, original_item);
let (original_item_candidate, trait_item_to_import) = match item_as_assoc {
Some(assoc_item) => match assoc_item.container(db) {
AssocItemContainer::Trait(trait_) => {
let trait_ = ItemInNs::from(ModuleDef::from(trait_));
(trait_, Some(trait_))
}
AssocItemContainer::Impl(impl_) => {
(ItemInNs::from(ModuleDef::from(impl_.self_ty(db).as_adt()?)), None)
}
},
None => (original_item, None),
};
let import_path_candidate = mod_path(original_item_candidate)?;
let mut import_path_candidate_segments = import_path_candidate.segments().iter().rev();
let predicate = |it: EitherOrBoth<&SmolStr, &Name>| match it {
// segments match, check next one
EitherOrBoth::Both(a, b) if b.as_str() == &**a => None,
// segments mismatch / qualifier is longer than the path, bail out
EitherOrBoth::Both(..) | EitherOrBoth::Left(_) => Some(false),
// all segments match and we have exhausted the qualifier, proceed
EitherOrBoth::Right(_) => Some(true),
};
if item_as_assoc.is_none() {
let item_name = item_name(db, original_item)?;
let last_segment = import_path_candidate_segments.next()?;
if *last_segment != item_name {
return None;
}
}
let ends_with = unresolved_qualifier
.iter()
.rev()
.zip_longest(import_path_candidate_segments)
.find_map(predicate)
.unwrap_or(true);
if !ends_with {
return None;
}
let segment_import = find_import_for_segment(db, original_item_candidate, first_segment)?;
Some(match (segment_import == original_item_candidate, trait_item_to_import) {
(true, Some(_)) => {
// FIXME we should be able to import both the trait and the segment,
// but it's unclear what to do with overlapping edits (merge imports?)
// especially in case of lazy completion edit resolutions.
return None;
}
(false, Some(trait_to_import)) if scope_filter(trait_to_import) => {
LocatedImport::new(mod_path(trait_to_import)?, trait_to_import, original_item)
}
(true, None) if scope_filter(original_item_candidate) => {
LocatedImport::new(import_path_candidate, original_item_candidate, original_item)
}
(false, None) if scope_filter(segment_import) => {
LocatedImport::new(mod_path(segment_import)?, segment_import, original_item)
let qualifier = {
let mut adjusted_resolved_qualifier = resolved_qualifier;
if !unresolved_qualifier.is_empty() {
match resolved_qualifier {
ItemInNs::Types(ModuleDef::Module(module)) => {
adjusted_resolved_qualifier = sema
.resolve_mod_path_relative(module, unresolved_qualifier.iter().cloned())?
.next()?;
}
// can't resolve multiple segments for non-module item path bases
_ => return None,
}
}
match adjusted_resolved_qualifier {
ItemInNs::Types(def) => def,
_ => return None,
}
};
let import_path_candidate = mod_path(resolved_qualifier)?;
let ty = match qualifier {
ModuleDef::Module(module) => {
return items_locator::items_with_name_in_module(
sema,
module,
candidate.clone(),
AssocSearchMode::Exclude,
)
.find(|&it| scope_filter(it))
.map(|item| LocatedImport::new(import_path_candidate, resolved_qualifier, item))
}
// FIXME
ModuleDef::Trait(_) => return None,
// FIXME
ModuleDef::TraitAlias(_) => return None,
ModuleDef::TypeAlias(alias) => alias.ty(sema.db),
ModuleDef::BuiltinType(builtin) => builtin.ty(sema.db),
ModuleDef::Adt(adt) => adt.ty(sema.db),
_ => return None,
};
ty.iterate_path_candidates(sema.db, scope, &FxHashSet::default(), None, None, |assoc| {
// FIXME: Support extra trait imports
if assoc.container_or_implemented_trait(sema.db).is_some() {
return None;
}
let name = assoc.name(sema.db)?;
let is_match = match candidate {
NameToImport::Prefix(text, true) => name.as_str().starts_with(text),
NameToImport::Prefix(text, false) => {
name.as_str().chars().zip(text.chars()).all(|(name_char, candidate_char)| {
name_char.eq_ignore_ascii_case(&candidate_char)
})
}
NameToImport::Exact(text, true) => name.as_str() == text,
NameToImport::Exact(text, false) => name.as_str().eq_ignore_ascii_case(text),
NameToImport::Fuzzy(text, true) => text.chars().all(|c| name.as_str().contains(c)),
NameToImport::Fuzzy(text, false) => text
.chars()
.all(|c| name.as_str().chars().any(|name_char| name_char.eq_ignore_ascii_case(&c))),
};
if !is_match {
return None;
}
Some(LocatedImport::new(
import_path_candidate.clone(),
resolved_qualifier,
assoc_to_item(assoc),
))
})
}
@ -453,45 +473,6 @@ fn item_for_path_search_assoc(db: &RootDatabase, assoc_item: AssocItem) -> Optio
})
}
fn find_import_for_segment(
db: &RootDatabase,
original_item: ItemInNs,
unresolved_first_segment: &str,
) -> Option<ItemInNs> {
let segment_is_name = item_name(db, original_item)
.map(|name| name.eq_ident(unresolved_first_segment))
.unwrap_or(false);
Some(if segment_is_name {
original_item
} else {
let matching_module =
module_with_segment_name(db, unresolved_first_segment, original_item)?;
ItemInNs::from(ModuleDef::from(matching_module))
})
}
fn module_with_segment_name(
db: &RootDatabase,
segment_name: &str,
candidate: ItemInNs,
) -> Option<Module> {
let mut current_module = match candidate {
ItemInNs::Types(module_def_id) => module_def_id.module(db),
ItemInNs::Values(module_def_id) => module_def_id.module(db),
ItemInNs::Macros(macro_def_id) => ModuleDef::from(macro_def_id).module(db),
};
while let Some(module) = current_module {
if let Some(module_name) = module.name(db) {
if module_name.eq_ident(segment_name) {
return Some(module);
}
}
current_module = module.parent(db);
}
None
}
fn trait_applicable_items(
sema: &Semantics<'_, RootDatabase>,
current_crate: Crate,
@ -703,7 +684,7 @@ impl ImportCandidate {
return None;
}
Some(ImportCandidate::Path(PathImportCandidate {
qualifier: None,
qualifier: vec![],
name: NameToImport::exact_case_sensitive(name.to_string()),
}))
}
@ -730,7 +711,7 @@ fn path_import_candidate(
.segments()
.map(|seg| seg.name_ref().map(|name| SmolStr::new(name.text())))
.collect::<Option<Vec<_>>>()?;
ImportCandidate::Path(PathImportCandidate { qualifier: Some(qualifier), name })
ImportCandidate::Path(PathImportCandidate { qualifier, name })
} else {
return None;
}
@ -754,10 +735,10 @@ fn path_import_candidate(
}
Some(_) => return None,
},
None => ImportCandidate::Path(PathImportCandidate { qualifier: None, name }),
None => ImportCandidate::Path(PathImportCandidate { qualifier: vec![], name }),
})
}
fn item_as_assoc(db: &RootDatabase, item: ItemInNs) -> Option<AssocItem> {
item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db))
item.into_module_def().as_assoc_item(db)
}

View file

@ -3,10 +3,14 @@
//! The main reason for this module to exist is the fact that project's items and dependencies' items
//! are located in different caches, with different APIs.
use either::Either;
use hir::{import_map, Crate, ItemInNs, Semantics};
use hir::{import_map, Crate, ItemInNs, Module, Semantics};
use limit::Limit;
use crate::{imports::import_assets::NameToImport, symbol_index, RootDatabase};
use crate::{
imports::import_assets::NameToImport,
symbol_index::{self, SymbolsDatabase as _},
RootDatabase,
};
/// A value to use, when uncertain which limit to pick.
pub static DEFAULT_QUERY_SEARCH_LIMIT: Limit = Limit::new(100);
@ -20,8 +24,7 @@ pub fn items_with_name<'a>(
name: NameToImport,
assoc_item_search: AssocSearchMode,
) -> impl Iterator<Item = ItemInNs> + 'a {
let krate_name = krate.display_name(sema.db).map(|name| name.to_string());
let _p = tracing::info_span!("items_with_name", name = name.text(), assoc_item_search = ?assoc_item_search, crate = ?krate_name)
let _p = tracing::info_span!("items_with_name", name = name.text(), assoc_item_search = ?assoc_item_search, crate = ?krate.display_name(sema.db).map(|name| name.to_string()))
.entered();
let prefix = matches!(name, NameToImport::Prefix(..));
@ -66,6 +69,54 @@ pub fn items_with_name<'a>(
find_items(sema, krate, local_query, external_query)
}
/// Searches for importable items with the given name in the crate and its dependencies.
pub fn items_with_name_in_module<'a>(
sema: &'a Semantics<'_, RootDatabase>,
module: Module,
name: NameToImport,
assoc_item_search: AssocSearchMode,
) -> impl Iterator<Item = ItemInNs> + 'a {
let _p = tracing::info_span!("items_with_name_in", name = name.text(), assoc_item_search = ?assoc_item_search, ?module)
.entered();
let prefix = matches!(name, NameToImport::Prefix(..));
let local_query = match name {
NameToImport::Prefix(exact_name, case_sensitive)
| NameToImport::Exact(exact_name, case_sensitive) => {
let mut local_query = symbol_index::Query::new(exact_name.clone());
local_query.assoc_search_mode(assoc_item_search);
if prefix {
local_query.prefix();
} else {
local_query.exact();
}
if case_sensitive {
local_query.case_sensitive();
}
local_query
}
NameToImport::Fuzzy(fuzzy_search_string, case_sensitive) => {
let mut local_query = symbol_index::Query::new(fuzzy_search_string.clone());
local_query.fuzzy();
local_query.assoc_search_mode(assoc_item_search);
if case_sensitive {
local_query.case_sensitive();
}
local_query
}
};
let mut local_results = Vec::new();
local_query.search(&[sema.db.module_symbols(module)], |local_candidate| {
local_results.push(match local_candidate.def {
hir::ModuleDef::Macro(macro_def) => ItemInNs::Macros(macro_def),
def => ItemInNs::from(def),
})
});
local_results.into_iter()
}
fn find_items<'a>(
sema: &'a Semantics<'_, RootDatabase>,
krate: Crate,