From ada5f2059c0171cdb92230f552320e9431630961 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 16 Dec 2024 13:17:59 +0100 Subject: [PATCH] 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. --- crates/hir/src/lib.rs | 6 +- crates/hir/src/semantics.rs | 19 +- .../src/handlers/qualify_method_call.rs | 2 +- .../ide-assists/src/handlers/qualify_path.rs | 10 +- .../replace_derive_with_manual_impl.rs | 2 +- .../src/handlers/wrap_return_type.rs | 2 +- crates/ide-completion/src/tests/flyimport.rs | 42 ++++ crates/ide-db/src/imports/import_assets.rs | 219 ++++++++---------- crates/ide-db/src/items_locator.rs | 59 ++++- 9 files changed, 224 insertions(+), 137 deletions(-) diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 3bc2eee1e7..dfc91c7343 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -3105,10 +3105,10 @@ impl From for ItemInNs { } impl ItemInNs { - pub fn as_module_def(self) -> Option { + 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), } } diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs index f030c1862a..c576d07a0e 100644 --- a/crates/hir/src/semantics.rs +++ b/crates/hir/src/semantics.rs @@ -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, + ) -> Option> { + 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 { self.analyze(record_lit.syntax())?.resolve_variant(self.db, record_lit) } diff --git a/crates/ide-assists/src/handlers/qualify_method_call.rs b/crates/ide-assists/src/handlers/qualify_method_call.rs index 14518c4d2c..c3600af5a6 100644 --- a/crates/ide-assists/src/handlers/qualify_method_call.rs +++ b/crates/ide-assists/src/handlers/qualify_method_call.rs @@ -86,7 +86,7 @@ fn item_for_path_search(db: &dyn HirDatabase, item: ItemInNs) -> Option Option { - item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db)) + item.into_module_def().as_assoc_item(db) } #[cfg(test)] diff --git a/crates/ide-assists/src/handlers/qualify_path.rs b/crates/ide-assists/src/handlers/qualify_path.rs index ac88861fe4..849b8a42c6 100644 --- a/crates/ide-assists/src/handlers/qualify_path.rs +++ b/crates/ide-assists/src/handlers/qualify_path.rs @@ -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 { - 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)), diff --git a/crates/ide-assists/src/handlers/replace_derive_with_manual_impl.rs b/crates/ide-assists/src/handlers/replace_derive_with_manual_impl.rs index 2dec876215..31e828eae2 100644 --- a/crates/ide-assists/src/handlers/replace_derive_with_manual_impl.rs +++ b/crates/ide-assists/src/handlers/replace_derive_with_manual_impl.rs @@ -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, }) diff --git a/crates/ide-assists/src/handlers/wrap_return_type.rs b/crates/ide-assists/src/handlers/wrap_return_type.rs index 2d918a5b1c..658600cd2d 100644 --- a/crates/ide-assists/src/handlers/wrap_return_type.rs +++ b/crates/ide-assists/src/handlers/wrap_return_type.rs @@ -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) diff --git a/crates/ide-completion/src/tests/flyimport.rs b/crates/ide-completion/src/tests/flyimport.rs index 4b949e0d65..f31b0d4910 100644 --- a/crates/ide-completion/src/tests/flyimport.rs +++ b/crates/ide-completion/src/tests/flyimport.rs @@ -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: 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: Src) -> Dst; + } +} +pub mod mem { + pub use crate::intrinsics::transmute; +} +//- /main.rs crate:main deps:core +fn function() { + mem::transmute$0 +} +"#, + expect![""], + ); +} diff --git a/crates/ide-db/src/imports/import_assets.rs b/crates/ide-db/src/imports/import_assets.rs index 82a182806a..dab36bf20b 100644 --- a/crates/ide-db/src/imports/import_assets.rs +++ b/crates/ide-db/src/imports/import_assets.rs @@ -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>, + pub qualifier: Vec, /// The name the item (struct, trait, enum, etc.) should have. pub name: NameToImport, } @@ -264,7 +263,6 @@ impl ImportAssets { Some(it) => it, None => return >::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 + Copy, @@ -322,8 +324,8 @@ fn path_applicable_imports( ) -> FxIndexSet { 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, + candidate: &NameToImport, + resolved_qualifier: ItemInNs, unresolved_qualifier: &[SmolStr], - original_item: ItemInNs, scope_filter: impl Fn(ItemInNs) -> bool, ) -> Option { 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_)) + 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, } - AssocItemContainer::Impl(impl_) => { - (ItemInNs::from(ModuleDef::from(impl_.self_ty(db).as_adt()?)), None) - } - }, - None => (original_item, None), + } + + match adjusted_resolved_qualifier { + ItemInNs::Types(def) => def, + _ => return 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 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 { - 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 { - 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::>>()?; - 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 { - item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db)) + item.into_module_def().as_assoc_item(db) } diff --git a/crates/ide-db/src/items_locator.rs b/crates/ide-db/src/items_locator.rs index 47549a1d00..7f66ea0c10 100644 --- a/crates/ide-db/src/items_locator.rs +++ b/crates/ide-db/src/items_locator.rs @@ -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 + '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 + '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,