Merge pull request #21464 from Veykril/push-myvkmuxpzkvp

fix: Fix path symbol search not respecting re-exports
This commit is contained in:
Lukas Wirth 2026-01-14 15:48:09 +00:00 committed by GitHub
commit 1790ab0cd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 23 deletions

View file

@ -610,6 +610,23 @@ impl Module {
res
}
pub fn modules_in_scope(&self, db: &dyn HirDatabase, pub_only: bool) -> Vec<(Name, Module)> {
let def_map = self.id.def_map(db);
let scope = &def_map[self.id].scope;
let mut res = Vec::new();
for (name, item) in scope.types() {
if let ModuleDefId::ModuleId(m) = item.def
&& (!pub_only || item.vis == Visibility::Public)
{
res.push((name.clone(), Module { id: m }));
}
}
res
}
/// Returns a `ModuleScope`: a set of items, visible in this module.
pub fn scope(
self,

View file

@ -35,6 +35,7 @@ use hir::{
import_map::{AssocSearchMode, SearchMode},
symbols::{FileSymbol, SymbolCollector},
};
use itertools::Itertools;
use rayon::prelude::*;
use salsa::Update;
@ -224,12 +225,10 @@ pub fn world_symbols(db: &RootDatabase, mut query: Query) -> Vec<FileSymbol<'_>>
// Search for crates by name (handles "::" and "::foo" queries)
let indices: Vec<_> = if query.is_crate_search() {
query.only_types = false;
query.libs = true;
vec![SymbolIndex::extern_prelude_symbols(db)]
// If we have a path filter, resolve it to target modules
} else if !query.path_filter.is_empty() {
query.only_types = false;
query.libs = true;
let target_modules = resolve_path_to_modules(
db,
&query.path_filter,
@ -313,11 +312,11 @@ fn resolve_path_to_modules(
// If anchor_to_crate is true, first segment MUST be a crate name
// If anchor_to_crate is false, first segment could be a crate OR a module in local crates
let mut candidate_modules: Vec<Module> = vec![];
let mut candidate_modules: Vec<(Module, bool)> = vec![];
// Add crate root modules for matching crates
for krate in matching_crates {
candidate_modules.push(krate.root_module(db));
candidate_modules.push((krate.root_module(db), krate.origin(db).is_local()));
}
// If not anchored to crate, also search for modules matching first segment in local crates
@ -329,7 +328,7 @@ fn resolve_path_to_modules(
if let Some(name) = child.name(db)
&& names_match(name.as_str(), first_segment)
{
candidate_modules.push(child);
candidate_modules.push((child, true));
}
}
}
@ -340,11 +339,14 @@ fn resolve_path_to_modules(
for segment in rest_segments {
candidate_modules = candidate_modules
.into_iter()
.flat_map(|module| {
module.children(db).filter(|child| {
child.name(db).is_some_and(|name| names_match(name.as_str(), segment))
})
.flat_map(|(module, local)| {
module
.modules_in_scope(db, !local)
.into_iter()
.filter(|(name, _)| names_match(name.as_str(), segment))
.map(move |(_, module)| (module, local))
})
.unique()
.collect();
if candidate_modules.is_empty() {
@ -352,7 +354,7 @@ fn resolve_path_to_modules(
}
}
candidate_modules
candidate_modules.into_iter().map(|(module, _)| module).collect()
}
#[derive(Default)]
@ -839,7 +841,7 @@ pub struct Foo;
assert_eq!(item, "foo");
assert!(anchor);
// Trailing :: (module browsing)
// Trailing ::
let (path, item, anchor) = Query::parse_path_query("foo::");
assert_eq!(path, vec!["foo"]);
assert_eq!(item, "");
@ -909,7 +911,7 @@ pub mod nested {
}
#[test]
fn test_module_browsing() {
fn test_path_search_module() {
let (mut db, _) = RootDatabase::with_many_files(
r#"
//- /lib.rs crate:main
@ -1066,20 +1068,11 @@ pub fn root_fn() {}
let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"RootItem"), "Expected RootItem at crate root in {:?}", names);
// Browse crate root
let query = Query::new("mylib::".to_owned());
let symbols = world_symbols(&db, query);
let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"RootItem"),
"Expected RootItem when browsing crate root in {:?}",
names
);
assert!(
names.contains(&"root_fn"),
"Expected root_fn when browsing crate root in {:?}",
names
);
assert!(names.contains(&"RootItem"), "Expected RootItem {:?}", names);
assert!(names.contains(&"root_fn"), "Expected root_fn {:?}", names);
}
#[test]
@ -1163,4 +1156,62 @@ pub struct FooStruct;
let symbols = world_symbols(&db, query);
assert!(symbols.is_empty(), "Expected empty results for non-matching crate pattern");
}
#[test]
fn test_path_search_with_use_reexport() {
// Test that module resolution works for `use` items (re-exports), not just `mod` items
let (mut db, _) = RootDatabase::with_many_files(
r#"
//- /lib.rs crate:main
mod inner;
pub use inner::nested;
//- /inner.rs
pub mod nested {
pub struct NestedStruct;
pub fn nested_fn() {}
}
"#,
);
let mut local_roots = FxHashSet::default();
local_roots.insert(WORKSPACE);
LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
// Search via the re-exported path (main::nested::NestedStruct)
// This should work because `nested` is in scope via `pub use inner::nested`
let query = Query::new("main::nested::NestedStruct".to_owned());
let symbols = world_symbols(&db, query);
let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"NestedStruct"),
"Expected NestedStruct via re-exported path in {:?}",
names
);
// Also verify the original path still works
let query = Query::new("main::inner::nested::NestedStruct".to_owned());
let symbols = world_symbols(&db, query);
let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"NestedStruct"),
"Expected NestedStruct via original path in {:?}",
names
);
// Browse the re-exported module
let query = Query::new("main::nested::".to_owned());
let symbols = world_symbols(&db, query);
let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"NestedStruct"),
"Expected NestedStruct when browsing re-exported module in {:?}",
names
);
assert!(
names.contains(&"nested_fn"),
"Expected nested_fn when browsing re-exported module in {:?}",
names
);
}
}