Skip to content

Commit 906fa6c

Browse files
authored
feat(lints): Emit unused_dependencies lint (#16600)
*[View all comments](https://triagebot.infra.rust-lang.org/gh-comments/rust-lang/cargo/pull/16600)* ### What does this PR try to resolve? Fixes #15813 ```toml [lints.cargo] unused_dependencies = "warn" ``` This checks only build and normal dependencies (see below for more details). I would expect we'd open a new issue to track dev-dependencies. I don't consider this a false-positive because there technically isn't a way to ask for everything that uses dev-dependencies to be built. This only checks selected packages, and not all local packages, and only if the build target selection flags are exhaustive without consideration for the selected packages. See #16600 (comment) Known false positives: - activating a feature on a transitive dependency - pinning a transitive dependency (instead use `target.cfg(false).dependencies`) To handle false positives, ```toml [lints.cargo] unused_dependencies = { level = "warn", ignore = ["dep_name"] } ``` - Some prior art use `ignored` but I felt like `ignore` fit in better with `allow` - Per-dep-table is more complicated for users, tricky to get the design right (`udeps` cares about dep kinds but not target cfgs), these shouldn't be too common, and using dep names gives users control over uniqueness - For more background on alternatives, see rust-lang/rfcs#3920 ### How to test and review this PR? Built-on #8437 This does nothing for dev-dependencies because there isn't really a way to select all targets today without - tracking selected dep kinds to check on a per-package basis - checking the status of every bench to see if it can work as a test because `cargo test` (no args) with benches set to test is the only command today that can exercise all dev-dependencies as it is the only one that will compile tests and doctests. See also - https://blog.rust-lang.org/inside-rust/2024/10/01/this-development-cycle-in-cargo-1.82/#detecting-unused-dependencies - https://blog.rust-lang.org/inside-rust/2024/10/01/this-development-cycle-in-cargo-1.82/#all-targets-and-doc-tests - https://blog.rust-lang.org/inside-rust/2024/10/31/this-development-cycle-in-cargo-1.83/#target-and-target As for the commits, I did something unusual for myself in that I made changes with dead code so it is easier to understand what goes into one step in this process without seeing the entire process at once and getting confused.
2 parents cc1f24f + ff4221a commit 906fa6c

22 files changed

Lines changed: 2460 additions & 29 deletions

File tree

src/cargo/core/compiler/build_context/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::core::compiler::CompileKind;
77
use crate::core::compiler::Unit;
88
use crate::core::compiler::UnitIndex;
99
use crate::core::compiler::unit_graph::UnitGraph;
10+
use crate::core::dependency::DepKind;
1011
use crate::core::profiles::Profiles;
1112
use crate::util::Rustc;
1213
use crate::util::context::GlobalContext;
@@ -64,6 +65,9 @@ pub struct BuildContext<'a, 'gctx> {
6465
/// Configuration information for a rustc build.
6566
pub build_config: &'a BuildConfig,
6667

68+
/// Associated [`DepKind`]s for root targets
69+
pub selected_dep_kinds: DepKindSet,
70+
6771
/// Extra compiler args for either `rustc` or `rustdoc`.
6872
pub extra_compiler_args: HashMap<Unit, Vec<String>>,
6973

@@ -97,6 +101,7 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> {
97101
logger: Option<&'a BuildLogger>,
98102
packages: PackageSet<'gctx>,
99103
build_config: &'a BuildConfig,
104+
selected_dep_kinds: DepKindSet,
100105
profiles: Profiles,
101106
extra_compiler_args: HashMap<Unit, Vec<String>>,
102107
target_data: RustcTargetData<'gctx>,
@@ -118,6 +123,7 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> {
118123
logger,
119124
packages,
120125
build_config,
126+
selected_dep_kinds,
121127
profiles,
122128
extra_compiler_args,
123129
target_data,
@@ -157,3 +163,20 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> {
157163
self.extra_compiler_args.get(unit)
158164
}
159165
}
166+
167+
#[derive(Copy, Clone, Default, Debug)]
168+
pub struct DepKindSet {
169+
pub build: bool,
170+
pub normal: bool,
171+
pub dev: bool,
172+
}
173+
174+
impl DepKindSet {
175+
pub fn contains(&self, kind: DepKind) -> bool {
176+
match kind {
177+
DepKind::Build => self.build,
178+
DepKind::Normal => self.normal,
179+
DepKind::Development => self.dev,
180+
}
181+
}
182+
}

src/cargo/core/compiler/job_queue/job_state.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,13 @@ impl<'a, 'gctx> JobState<'a, 'gctx> {
219219
self.messages
220220
.push(Message::FutureIncompatReport(self.id, report));
221221
}
222+
223+
/// The rustc emitted the list of unused `--extern` args.
224+
///
225+
/// This is useful for checking unused dependencies.
226+
/// Should only be called once, as the compiler only emits it once per compilation.
227+
pub fn unused_externs(&self, unused_externs: Vec<String>) {
228+
self.messages
229+
.push(Message::UnusedExterns(self.id, unused_externs));
230+
}
222231
}

src/cargo/core/compiler/job_queue/mod.rs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ use super::UnitIndex;
138138
use super::custom_build::Severity;
139139
use super::timings::SectionTiming;
140140
use super::timings::Timings;
141+
use super::unused_deps::UnusedDepState;
141142
use crate::core::compiler::descriptive_pkg_name;
142143
use crate::core::compiler::future_incompat::{
143144
self, FutureBreakageItem, FutureIncompatReportPackage,
@@ -187,6 +188,7 @@ struct DrainState<'gctx> {
187188
progress: Progress<'gctx>,
188189
next_id: u32,
189190
timings: Timings<'gctx>,
191+
unused_dep_state: UnusedDepState,
190192

191193
/// Map from unit index to unit, for looking up dependency information.
192194
index_to_unit: HashMap<UnitIndex, Unit>,
@@ -385,6 +387,7 @@ enum Message {
385387
Finish(JobId, Artifact, CargoResult<()>),
386388
FutureIncompatReport(JobId, Vec<FutureBreakageItem>),
387389
SectionTiming(JobId, SectionTiming),
390+
UnusedExterns(JobId, Vec<String>),
388391
}
389392

390393
impl<'gctx> JobQueue<'gctx> {
@@ -504,6 +507,7 @@ impl<'gctx> JobQueue<'gctx> {
504507
progress,
505508
next_id: 0,
506509
timings: self.timings,
510+
unused_dep_state: UnusedDepState::new(build_runner),
507511
index_to_unit: build_runner
508512
.bcx
509513
.unit_to_index
@@ -544,12 +548,10 @@ impl<'gctx> JobQueue<'gctx> {
544548
.take()
545549
.map(move |srv| srv.start(move |msg| messages.push(Message::FixDiagnostic(msg))));
546550

547-
thread::scope(
548-
move |scope| match state.drain_the_queue(build_runner, scope, &helper) {
549-
Some(err) => Err(err),
550-
None => Ok(()),
551-
},
552-
)
551+
thread::scope(move |scope| {
552+
let (result,) = state.drain_the_queue(build_runner, scope, &helper);
553+
result
554+
})
553555
}
554556
}
555557

@@ -743,6 +745,11 @@ impl<'gctx> DrainState<'gctx> {
743745
items,
744746
});
745747
}
748+
Message::UnusedExterns(id, unused_externs) => {
749+
let unit = &self.active[&id];
750+
self.unused_dep_state
751+
.record_unused_externs_for_unit(unit, unused_externs);
752+
}
746753
Message::Token(acquired_token) => {
747754
let token = acquired_token.context("failed to acquire jobserver token")?;
748755
self.tokens.push(token);
@@ -783,14 +790,15 @@ impl<'gctx> DrainState<'gctx> {
783790
/// This is the "main" loop, where Cargo does all work to run the
784791
/// compiler.
785792
///
786-
/// This returns an Option to prevent the use of `?` on `Result` types
787-
/// because it is important for the loop to carefully handle errors.
793+
/// This returns a tuple of `Result` to prevent the use of `?` on
794+
/// `Result` types because it is important for the loop to
795+
/// carefully handle errors.
788796
fn drain_the_queue<'s>(
789797
mut self,
790798
build_runner: &mut BuildRunner<'_, '_>,
791799
scope: &'s Scope<'s, '_>,
792800
jobserver_helper: &HelperThread,
793-
) -> Option<anyhow::Error> {
801+
) -> (Result<(), anyhow::Error>,) {
794802
trace!("queue: {:#?}", self.queue);
795803

796804
// Iteratively execute the entire dependency graph. Each turn of the
@@ -834,6 +842,18 @@ impl<'gctx> DrainState<'gctx> {
834842
}
835843
self.progress.clear();
836844

845+
if build_runner.bcx.gctx.cli_unstable().cargo_lints {
846+
let mut warn_count = 0;
847+
let mut error_count = 0;
848+
drop(self.unused_dep_state.emit_unused_warnings(
849+
&mut warn_count,
850+
&mut error_count,
851+
build_runner,
852+
));
853+
errors.count += error_count;
854+
build_runner.compilation.lint_warning_count += warn_count;
855+
}
856+
837857
let profile_name = build_runner.bcx.build_config.requested_profile;
838858
// NOTE: this may be a bit inaccurate, since this may not display the
839859
// profile for what was actually built. Profile overrides can change
@@ -874,7 +894,7 @@ impl<'gctx> DrainState<'gctx> {
874894
if let Some(error) = errors.to_error() {
875895
// Any errors up to this point have already been printed via the
876896
// `display_error` inside `handle_error`.
877-
Some(anyhow::Error::new(AlreadyPrintedError::new(error)))
897+
(Err(anyhow::Error::new(AlreadyPrintedError::new(error))),)
878898
} else if self.queue.is_empty() && self.pending_queue.is_empty() {
879899
let profile_link = build_runner.bcx.gctx.shell().err_hyperlink(
880900
"https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles",
@@ -889,10 +909,10 @@ impl<'gctx> DrainState<'gctx> {
889909
&self.per_package_future_incompat_reports,
890910
);
891911

892-
None
912+
(Ok(()),)
893913
} else {
894914
debug!("queue: {:#?}", self.queue);
895-
Some(internal("finished with jobs still left in the queue"))
915+
(Err(internal("finished with jobs still left in the queue")),)
896916
}
897917
}
898918

src/cargo/core/compiler/mod.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub mod timings;
5151
mod unit;
5252
pub mod unit_dependencies;
5353
pub mod unit_graph;
54+
mod unused_deps;
5455

5556
use std::borrow::Cow;
5657
use std::cell::OnceCell;
@@ -74,6 +75,7 @@ use tracing::{debug, instrument, trace};
7475
pub use self::build_config::UserIntent;
7576
pub use self::build_config::{BuildConfig, CompileMode, MessageFormat};
7677
pub use self::build_context::BuildContext;
78+
pub use self::build_context::DepKindSet;
7779
pub use self::build_context::FileFlavor;
7880
pub use self::build_context::FileType;
7981
pub use self::build_context::RustcTargetData;
@@ -832,6 +834,12 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult
832834
base.env("CARGO_TARGET_TMPDIR", tmp.display().to_string());
833835
}
834836

837+
if build_runner.bcx.gctx.cli_unstable().cargo_lints {
838+
// Added last to reduce the risk of RUSTFLAGS or `[lints]` from interfering with
839+
// `unused_dependencies` tracking
840+
base.arg("-Wunused_crate_dependencies");
841+
}
842+
835843
Ok(base)
836844
}
837845

@@ -1175,8 +1183,11 @@ fn add_error_format_and_color(build_runner: &BuildRunner<'_, '_>, cmd: &mut Proc
11751183
}
11761184

11771185
cmd.arg("--error-format=json");
1178-
let mut json = String::from("--json=diagnostic-rendered-ansi,artifacts,future-incompat");
11791186

1187+
let mut json = String::from("--json=diagnostic-rendered-ansi,artifacts,future-incompat");
1188+
if build_runner.bcx.gctx.cli_unstable().cargo_lints {
1189+
json.push_str(",unused-externs-silent");
1190+
}
11801191
if let MessageFormat::Short | MessageFormat::Json { short: true, .. } =
11811192
build_runner.bcx.build_config.message_format
11821193
{
@@ -1186,11 +1197,9 @@ fn add_error_format_and_color(build_runner: &BuildRunner<'_, '_>, cmd: &mut Proc
11861197
{
11871198
json.push_str(",diagnostic-unicode");
11881199
}
1189-
11901200
if enable_timings {
11911201
json.push_str(",timings");
11921202
}
1193-
11941203
cmd.arg(json);
11951204

11961205
let gctx = build_runner.bcx.gctx;
@@ -2376,6 +2385,19 @@ fn on_stderr_line_inner(
23762385
return Ok(false);
23772386
}
23782387

2388+
#[derive(serde::Deserialize)]
2389+
struct UnusedExterns {
2390+
unused_extern_names: Vec<String>,
2391+
}
2392+
if let Ok(uext) = serde_json::from_str::<UnusedExterns>(compiler_message.get()) {
2393+
trace!(
2394+
"obtained unused externs list from rustc: `{:?}`",
2395+
uext.unused_extern_names
2396+
);
2397+
state.unused_externs(uext.unused_extern_names);
2398+
return Ok(true);
2399+
}
2400+
23792401
// And failing all that above we should have a legitimate JSON diagnostic
23802402
// from the compiler, so wrap it in an external Cargo JSON message
23812403
// indicating which package it came from and then emit it.

0 commit comments

Comments
 (0)