From 36ae25a52c9577850d6b9abca4ad8c60f696f9ba Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 22 Apr 2026 15:59:51 -0500 Subject: [PATCH 1/3] test(lints): Make package selection test resilient against change This is to ensure these tests still have the intended coverage regardless of how we work around unused direct deps that are present to control their transitive presence. --- tests/testsuite/lints/unused_dependencies.rs | 56 +++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/testsuite/lints/unused_dependencies.rs b/tests/testsuite/lints/unused_dependencies.rs index 8c7b915d3b8..b740867ddca 100644 --- a/tests/testsuite/lints/unused_dependencies.rs +++ b/tests/testsuite/lints/unused_dependencies.rs @@ -968,7 +968,9 @@ fn package_selection() { Package::new("used_bar", "0.1.0").publish(); Package::new("used_foo", "0.1.0").publish(); Package::new("used_external", "0.1.0").publish(); - Package::new("unused", "0.1.0").publish(); + Package::new("unused_bar", "0.1.0").publish(); + Package::new("unused_foo", "0.1.0").publish(); + Package::new("unused_external", "0.1.0").publish(); let p = project() .file( "Cargo.toml", @@ -988,7 +990,7 @@ fn package_selection() { edition = "2018" [dependencies] - unused = "0.1.0" + unused_foo = "0.1.0" used_foo = "0.1.0" bar.path = "../bar" external.path = "../external" @@ -1013,7 +1015,7 @@ fn package_selection() { edition = "2018" [dependencies] - unused = "0.1.0" + unused_bar = "0.1.0" used_bar = "0.1.0" [lints.cargo] @@ -1036,7 +1038,7 @@ fn package_selection() { edition = "2018" [dependencies] - unused = "0.1.0" + unused_external = "0.1.0" used_external = "0.1.0" [lints.cargo] @@ -1056,30 +1058,33 @@ fn package_selection() { .with_stderr_data( str![[r#" [UPDATING] `dummy-registry` index +[LOCKING] 7 packages to latest compatible versions [DOWNLOADING] crates ... -[CHECKING] bar v0.1.0 ([ROOT]/foo/bar) -[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s -[LOCKING] 5 packages to latest compatible versions -[DOWNLOADED] used_foo v0.1.0 (registry `dummy-registry`) -[DOWNLOADED] used_external v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unused_bar v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unused_external v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unused_foo v0.1.0 (registry `dummy-registry`) [DOWNLOADED] used_bar v0.1.0 (registry `dummy-registry`) -[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) -[CHECKING] unused v0.1.0 +[DOWNLOADED] used_external v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] used_foo v0.1.0 (registry `dummy-registry`) [CHECKING] used_bar v0.1.0 +[CHECKING] unused_external v0.1.0 [CHECKING] used_external v0.1.0 +[CHECKING] unused_bar v0.1.0 [CHECKING] used_foo v0.1.0 +[CHECKING] unused_foo v0.1.0 +[CHECKING] bar v0.1.0 ([ROOT]/foo/bar) [CHECKING] external v0.1.0 ([ROOT]/foo/external) [CHECKING] foo v0.1.0 ([ROOT]/foo/foo) [WARNING] unused dependency --> bar/Cargo.toml:9:13 | -9 | unused = "0.1.0" - | ^^^^^^^^^^^^^^^^ +9 | unused_bar = "0.1.0" + | ^^^^^^^^^^^^^^^^^^^^ | = [NOTE] `cargo::unused_dependencies` is set to `warn` in `[lints]` [HELP] remove the dependency | -9 - unused = "0.1.0" +9 - unused_bar = "0.1.0" | [WARNING] unused dependency --> foo/Cargo.toml:11:13 @@ -1107,13 +1112,14 @@ fn package_selection() { [WARNING] unused dependency --> foo/Cargo.toml:9:13 | -9 | unused = "0.1.0" - | ^^^^^^^^^^^^^^^^ +9 | unused_foo = "0.1.0" + | ^^^^^^^^^^^^^^^^^^^^ | [HELP] remove the dependency | -9 - unused = "0.1.0" +9 - unused_foo = "0.1.0" | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), @@ -1124,7 +1130,6 @@ fn package_selection() { .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data( str![[r#" -[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s [WARNING] unused dependency --> foo/Cargo.toml:11:13 | @@ -1151,13 +1156,14 @@ fn package_selection() { [WARNING] unused dependency --> foo/Cargo.toml:9:13 | -9 | unused = "0.1.0" - | ^^^^^^^^^^^^^^^^ +9 | unused_foo = "0.1.0" + | ^^^^^^^^^^^^^^^^^^^^ | [HELP] remove the dependency | -9 - unused = "0.1.0" +9 - unused_foo = "0.1.0" | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), @@ -1168,18 +1174,18 @@ fn package_selection() { .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data( str![[r#" -[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s [WARNING] unused dependency --> bar/Cargo.toml:9:13 | -9 | unused = "0.1.0" - | ^^^^^^^^^^^^^^^^ +9 | unused_bar = "0.1.0" + | ^^^^^^^^^^^^^^^^^^^^ | = [NOTE] `cargo::unused_dependencies` is set to `warn` in `[lints]` [HELP] remove the dependency | -9 - unused = "0.1.0" +9 - unused_bar = "0.1.0" | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), From 5442d4942a9ccfabf0f1e2845a386c7a9c121fd2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 16 Apr 2026 16:07:11 -0500 Subject: [PATCH 2/3] refactor(compile): Track manifest deps in ExternState --- src/cargo/core/compiler/unused_deps.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/cargo/core/compiler/unused_deps.rs b/src/cargo/core/compiler/unused_deps.rs index 39b27f724da..a1e3df1f266 100644 --- a/src/cargo/core/compiler/unused_deps.rs +++ b/src/cargo/core/compiler/unused_deps.rs @@ -98,7 +98,9 @@ impl UnusedDepState { } else { continue; }; - state.externs.insert(dep.extern_crate_name, manifest_deps); + state + .externs + .insert(dep.extern_crate_name, ExternState { manifest_deps }); } } @@ -213,7 +215,7 @@ impl UnusedDepState { continue; } - for (ext, dependency) in &state.externs { + for (ext, extern_state) in &state.externs { if state .unused_externs .values() @@ -229,7 +231,7 @@ impl UnusedDepState { } // Implicitly added dependencies (in the same crate) aren't interesting - let dependency = if let Some(dependency) = dependency { + let dependency = if let Some(dependency) = &extern_state.manifest_deps { dependency } else { continue; @@ -318,7 +320,7 @@ impl UnusedDepState { #[derive(Default)] struct DependenciesState { /// All declared dependencies - externs: IndexMap>>, + externs: IndexMap, /// Expected [`Self::unused_externs`] entries to know we've received them all /// /// To avoid warning in cases where we didn't, @@ -328,6 +330,11 @@ struct DependenciesState { unused_externs: IndexMap>, } +#[derive(Clone)] +struct ExternState { + manifest_deps: Option>, +} + fn dep_kind_of(unit: &Unit) -> DepKind { match unit.target.kind() { TargetKind::Lib(_) => match unit.mode { From 944a5403d3916f09eb6d12965ef7889ba72dd9d1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 22 Apr 2026 14:04:06 -0500 Subject: [PATCH 3/3] fix(compile): Ignore unused deps if also transitive This is to reduce false positives without having to ignore them. In fact, I plan to revert support for `ignore` after this change. --- src/cargo/core/compiler/unused_deps.rs | 50 ++++++++++++++++++-- tests/testsuite/lints/unused_dependencies.rs | 11 ----- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/cargo/core/compiler/unused_deps.rs b/src/cargo/core/compiler/unused_deps.rs index a1e3df1f266..519bd6cb6ea 100644 --- a/src/cargo/core/compiler/unused_deps.rs +++ b/src/cargo/core/compiler/unused_deps.rs @@ -98,9 +98,13 @@ impl UnusedDepState { } else { continue; }; - state - .externs - .insert(dep.extern_crate_name, ExternState { manifest_deps }); + state.externs.insert( + dep.extern_crate_name, + ExternState { + unit: dep.unit.clone(), + manifest_deps, + }, + ); } } @@ -229,6 +233,14 @@ impl UnusedDepState { ); continue; } + if is_transitive_dep(&extern_state.unit, &state.unused_externs, build_runner) { + debug!( + "pkg {} v{} ({dep_kind:?}): ignoring unused extern `{ext}`, may be activating features", + pkg_id.name(), + pkg_id.version(), + ); + continue; + } // Implicitly added dependencies (in the same crate) aren't interesting let dependency = if let Some(dependency) = &extern_state.manifest_deps { @@ -332,6 +344,7 @@ struct DependenciesState { #[derive(Clone)] struct ExternState { + unit: Unit, manifest_deps: Option>, } @@ -359,3 +372,34 @@ fn unit_desc(unit: &Unit) -> String { unit.mode, ) } + +#[instrument(skip_all)] +fn is_transitive_dep( + direct_dep_unit: &Unit, + unused_externs: &IndexMap>, + build_runner: &mut BuildRunner<'_, '_>, +) -> bool { + let mut queue = std::collections::VecDeque::new(); + for root_unit in unused_externs.keys() { + for unit_dep in build_runner.unit_deps(root_unit) { + if root_unit.pkg.package_id() == unit_dep.unit.pkg.package_id() { + continue; + } + if unit_dep.unit == *direct_dep_unit { + continue; + } + queue.push_back(&unit_dep.unit); + } + } + + while let Some(dep_unit) = queue.pop_front() { + for unit_dep in build_runner.unit_deps(dep_unit) { + if unit_dep.unit == *direct_dep_unit { + return true; + } + queue.push_back(&unit_dep.unit); + } + } + + false +} diff --git a/tests/testsuite/lints/unused_dependencies.rs b/tests/testsuite/lints/unused_dependencies.rs index b740867ddca..664950f6ef0 100644 --- a/tests/testsuite/lints/unused_dependencies.rs +++ b/tests/testsuite/lints/unused_dependencies.rs @@ -1320,17 +1320,6 @@ pub fn fun() -> &'static str { [CHECKING] transitive v0.1.1 [CHECKING] intermediate v0.1.0 [CHECKING] foo v0.1.0 ([ROOT]/foo) -[WARNING] unused dependency - --> Cargo.toml:10:13 - | -10 | transitive = { version = "0.1.1", features = ["a"] } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unused_dependencies` is set to `warn` in `[lints]` -[HELP] remove the dependency - | -10 - transitive = { version = "0.1.1", features = ["a"] } - | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]