Merge pull request #21574 from ChayimFriedman2/autoimport-after

feat: When autoimporting a segment followed by other segments, only consider items that will resolve with the after segments
This commit is contained in:
Shoyu Vanilla (Flint) 2026-02-09 14:08:56 +00:00 committed by GitHub
commit 7d521a2cd9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 11 deletions

View file

@ -6068,11 +6068,7 @@ impl<'db> Type<'db> {
match name {
Some(name) => {
match ctx.probe_for_name(
method_resolution::Mode::MethodCall,
name.clone(),
self_ty,
) {
match ctx.probe_for_name(method_resolution::Mode::Path, name.clone(), self_ty) {
Ok(candidate)
| Err(method_resolution::MethodError::PrivateMatch(candidate)) => {
let id = candidate.item.into();

View file

@ -352,7 +352,7 @@ mod tests {
let config = TEST_CONFIG;
let ctx = AssistContext::new(sema, &config, frange);
let mut acc = Assists::new(&ctx, AssistResolveStrategy::All);
auto_import(&mut acc, &ctx);
hir::attach_db(&db, || auto_import(&mut acc, &ctx));
let assists = acc.finish();
let labels = assists.iter().map(|assist| assist.label.to_string()).collect::<Vec<_>>();
@ -1897,4 +1897,35 @@ fn foo(_: S) {}
"#,
);
}
#[test]
fn with_after_segments() {
let before = r#"
mod foo {
pub mod wanted {
pub fn abc() {}
}
}
mod bar {
pub mod wanted {}
}
mod baz {
pub fn wanted() {}
}
mod quux {
pub struct wanted;
}
impl quux::wanted {
fn abc() {}
}
fn f() {
wanted$0::abc;
}
"#;
check_auto_import_order(before, &["Import `foo::wanted`", "Import `quux::wanted`"]);
}
}

View file

@ -9,7 +9,7 @@ use hir::{
};
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use smallvec::{SmallVec, smallvec};
use syntax::{
AstNode, SyntaxNode,
ast::{self, HasName, make},
@ -68,6 +68,8 @@ pub struct PathImportCandidate {
pub qualifier: Vec<Name>,
/// The name the item (struct, trait, enum, etc.) should have.
pub name: NameToImport,
/// Potentially more segments that should resolve in the candidate.
pub after: Vec<Name>,
}
/// A name that will be used during item lookups.
@ -376,7 +378,7 @@ fn path_applicable_imports(
) -> FxIndexSet<LocatedImport> {
let _p = tracing::info_span!("ImportAssets::path_applicable_imports").entered();
match &*path_candidate.qualifier {
let mut result = match &*path_candidate.qualifier {
[] => {
items_locator::items_with_name(
db,
@ -433,6 +435,75 @@ fn path_applicable_imports(
})
.take(DEFAULT_QUERY_SEARCH_LIMIT)
.collect(),
};
filter_candidates_by_after_path(db, scope, path_candidate, &mut result);
result
}
fn filter_candidates_by_after_path(
db: &RootDatabase,
scope: &SemanticsScope<'_>,
path_candidate: &PathImportCandidate,
imports: &mut FxIndexSet<LocatedImport>,
) {
if imports.len() <= 1 {
// Short-circuit, as even if it doesn't match fully we want it.
return;
}
let Some((last_after, after_except_last)) = path_candidate.after.split_last() else {
return;
};
let original_imports = imports.clone();
let traits_in_scope = scope.visible_traits();
imports.retain(|import| {
let items = if after_except_last.is_empty() {
smallvec![import.original_item]
} else {
let ItemInNs::Types(ModuleDef::Module(item)) = import.original_item else {
return false;
};
// FIXME: This doesn't consider visibilities.
item.resolve_mod_path(db, after_except_last.iter().cloned())
.into_iter()
.flatten()
.collect::<SmallVec<[_; 3]>>()
};
items.into_iter().any(|item| {
let has_last_method = |ty: hir::Type<'_>| {
ty.iterate_path_candidates(db, scope, &traits_in_scope, Some(last_after), |_| {
Some(())
})
.is_some()
};
// FIXME: A trait can have an assoc type that has a function/const, that's two segments before last.
match item {
// A module? Can we resolve one more segment?
ItemInNs::Types(ModuleDef::Module(module)) => module
.resolve_mod_path(db, [last_after.clone()])
.is_some_and(|mut it| it.any(|_| true)),
// And ADT/Type Alias? That might be a method.
ItemInNs::Types(ModuleDef::Adt(it)) => has_last_method(it.ty(db)),
ItemInNs::Types(ModuleDef::BuiltinType(it)) => has_last_method(it.ty(db)),
ItemInNs::Types(ModuleDef::TypeAlias(it)) => has_last_method(it.ty(db)),
// A trait? Might have an associated item.
ItemInNs::Types(ModuleDef::Trait(it)) => it
.items(db)
.into_iter()
.any(|assoc_item| assoc_item.name(db) == Some(last_after.clone())),
// Other items? can't resolve one more segment.
_ => false,
}
})
});
if imports.is_empty() {
// Better one half-match than zero full matches.
*imports = original_imports;
}
}
@ -759,10 +830,14 @@ impl<'db> ImportCandidate<'db> {
if sema.resolve_path(path).is_some() {
return None;
}
let after = std::iter::successors(path.parent_path(), |it| it.parent_path())
.map(|seg| seg.segment()?.name_ref().map(|name| Name::new_root(&name.text())))
.collect::<Option<_>>()?;
path_import_candidate(
sema,
path.qualifier(),
NameToImport::exact_case_sensitive(path.segment()?.name_ref()?.to_string()),
after,
)
}
@ -777,6 +852,7 @@ impl<'db> ImportCandidate<'db> {
Some(ImportCandidate::Path(PathImportCandidate {
qualifier: vec![],
name: NameToImport::exact_case_sensitive(name.to_string()),
after: vec![],
}))
}
@ -785,7 +861,8 @@ impl<'db> ImportCandidate<'db> {
fuzzy_name: String,
sema: &Semantics<'db, RootDatabase>,
) -> Option<Self> {
path_import_candidate(sema, qualifier, NameToImport::fuzzy(fuzzy_name))
// Assume a fuzzy match does not want the segments after. Because... I guess why not?
path_import_candidate(sema, qualifier, NameToImport::fuzzy(fuzzy_name), Vec::new())
}
}
@ -793,6 +870,7 @@ fn path_import_candidate<'db>(
sema: &Semantics<'db, RootDatabase>,
qualifier: Option<ast::Path>,
name: NameToImport,
after: Vec<Name>,
) -> Option<ImportCandidate<'db>> {
Some(match qualifier {
Some(qualifier) => match sema.resolve_path(&qualifier) {
@ -802,7 +880,7 @@ fn path_import_candidate<'db>(
.segments()
.map(|seg| seg.name_ref().map(|name| Name::new_root(&name.text())))
.collect::<Option<Vec<_>>>()?;
ImportCandidate::Path(PathImportCandidate { qualifier, name })
ImportCandidate::Path(PathImportCandidate { qualifier, name, after })
} else {
return None;
}
@ -826,7 +904,7 @@ fn path_import_candidate<'db>(
}
Some(_) => return None,
},
None => ImportCandidate::Path(PathImportCandidate { qualifier: vec![], name }),
None => ImportCandidate::Path(PathImportCandidate { qualifier: vec![], name, after }),
})
}