From 6875998ab6c0f4c81eeb88d7051f4e8b4b39984b Mon Sep 17 00:00:00 2001 From: endergebnis Date: Fri, 4 Sep 2026 03:07:47 +0200 Subject: [PATCH] Fix crate-name import resolution for bin+lib crates Register the Cargo package name (normalized - -> _) as an alias for src/lib.rs in the resolver, so 'use ::{...}' in src/main.rs resolves to src/lib.rs. Adds rustdemo/Cargo.toml fixture and test coverage; updates blast-radius test for the new transitive edge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/index/imports.rs | 13 ++++++++++++- src/index/pipeline.rs | 30 ++++++++++++++++++++++++++++-- tests/cli.rs | 22 ++++++++++++++++++++-- tests/fixtures/rustdemo/Cargo.toml | 4 ++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/rustdemo/Cargo.toml diff --git a/src/index/imports.rs b/src/index/imports.rs index 3eaa0d6..521f30a 100644 --- a/src/index/imports.rs +++ b/src/index/imports.rs @@ -294,7 +294,7 @@ fn clean(target: &str) -> Option { } impl Resolver { - pub fn build(files: Vec<(String, i64)>) -> Self { + pub fn build(files: Vec<(String, i64)>, crate_name: Option<&str>) -> Self { let mut by_path = HashMap::new(); let mut by_name = HashMap::new(); let mut by_stem = HashMap::new(); @@ -308,6 +308,17 @@ impl Resolver { .unwrap_or(name); by_stem.entry(stem).or_insert(id); } + // `use ::…` names the crate's own lib root, which always lives + // at src/lib.rs — there is no `.rs` file. Register the package + // name as an alias so bin→lib edges resolve. Workspaces (no [package] + // name) are skipped. + if let Some(name) = crate_name.filter(|n| !n.is_empty()) + && let Some(id) = by_path.get("src/lib.rs").copied() + { + by_path.insert(name.to_string(), id); + by_name.entry(name.to_string()).or_insert(id); + by_stem.entry(name.to_string()).or_insert(id); + } Resolver { by_path, by_name, diff --git a/src/index/pipeline.rs b/src/index/pipeline.rs index 4647efd..494414c 100644 --- a/src/index/pipeline.rs +++ b/src/index/pipeline.rs @@ -18,6 +18,30 @@ pub struct IndexStats { pub imports: i64, } +/// Read the `[package] name` from `Cargo.toml`, normalized to the crate +/// identifier (`-` → `_`) used in `use ::…`. +fn read_crate_name(project: &Path) -> Option { + let toml = std::fs::read_to_string(project.join("Cargo.toml")).ok()?; + let mut in_package = false; + for line in toml.lines() { + let l = line.trim(); + if l.starts_with('[') { + in_package = l == "[package]"; + continue; + } + if in_package + && let Some(v) = l.strip_prefix("name").map(str::trim) + && let Some(v) = v.strip_prefix('=').map(str::trim) + { + let name = v.trim_matches('"'); + if !name.is_empty() { + return Some(name.replace('-', "_")); + } + } + } + None +} + /// (Re)index a project. `recreate` drops any existing index first. pub fn run(project: &Path, db_path: &Path, recreate: bool) -> Result { if recreate && db_path.exists() { @@ -83,8 +107,10 @@ pub fn run(project: &Path, db_path: &Path, recreate: bool) -> Result } } - let resolver = - imports::Resolver::build(file_ids.iter().map(|(k, v)| (k.clone(), *v)).collect()); + let resolver = imports::Resolver::build( + file_ids.iter().map(|(k, v)| (k.clone(), *v)).collect(), + read_crate_name(project).as_deref(), + ); for f in &changed { let fid = file_ids[&f.rel]; diff --git a/tests/cli.rs b/tests/cli.rs index 31968c4..dec88b0 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -90,6 +90,22 @@ fn rust_index_search_and_dead() { "source contains function body" ); + // `use rustdemo::add` in main.rs resolves via the package-name alias. + let importers = run_json(&[ + "--project", + fx.to_str().unwrap(), + "find", + "importers", + "src/lib.rs", + ]); + let names: Vec<&str> = importers + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert!(names.contains(&"src/main.rs"), "main.rs imports lib.rs"); + // dead.rs is flagged; entry points lib.rs/main.rs are not. let dead = run_json(&["--project", fx.to_str().unwrap(), "find", "dead"]); let paths: Vec<&str> = dead["files"] @@ -118,7 +134,7 @@ fn inspect_blast_radius_finds_affected_files() { "blast-radius", "src/util.rs", ]); - assert_eq!(blast["count"], 1); + assert_eq!(blast["count"], 2); assert_eq!(blast["file"], "src/util.rs"); let affected: Vec<&str> = blast["affected"] .as_array() @@ -126,7 +142,9 @@ fn inspect_blast_radius_finds_affected_files() { .iter() .map(|f| f["path"].as_str().unwrap()) .collect(); - assert_eq!(affected, ["src/lib.rs"], "lib.rs imports util.rs"); + // lib.rs imports util.rs directly; main.rs reaches it via the crate name. + assert!(affected.contains(&"src/lib.rs"), "lib.rs imports util.rs"); + assert!(affected.contains(&"src/main.rs"), "main.rs → lib.rs → util.rs"); } #[test] diff --git a/tests/fixtures/rustdemo/Cargo.toml b/tests/fixtures/rustdemo/Cargo.toml new file mode 100644 index 0000000..ebf47cb --- /dev/null +++ b/tests/fixtures/rustdemo/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "rustdemo" +version = "0.1.0" +edition = "2021"