diff --git a/src/cargo/core/compiler/build_context/mod.rs b/src/cargo/core/compiler/build_context/mod.rs index 2f09b4266ec..edca6d5d2ac 100644 --- a/src/cargo/core/compiler/build_context/mod.rs +++ b/src/cargo/core/compiler/build_context/mod.rs @@ -7,6 +7,7 @@ use crate::core::compiler::CompileKind; use crate::core::compiler::Unit; use crate::core::compiler::UnitIndex; use crate::core::compiler::unit_graph::UnitGraph; +use crate::core::dependency::DepKind; use crate::core::profiles::Profiles; use crate::util::Rustc; use crate::util::context::GlobalContext; @@ -64,6 +65,9 @@ pub struct BuildContext<'a, 'gctx> { /// Configuration information for a rustc build. pub build_config: &'a BuildConfig, + /// Associated [`DepKind`]s for root targets + pub selected_dep_kinds: DepKindSet, + /// Extra compiler args for either `rustc` or `rustdoc`. pub extra_compiler_args: HashMap>, @@ -97,6 +101,7 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> { logger: Option<&'a BuildLogger>, packages: PackageSet<'gctx>, build_config: &'a BuildConfig, + selected_dep_kinds: DepKindSet, profiles: Profiles, extra_compiler_args: HashMap>, target_data: RustcTargetData<'gctx>, @@ -118,6 +123,7 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> { logger, packages, build_config, + selected_dep_kinds, profiles, extra_compiler_args, target_data, @@ -157,3 +163,20 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> { self.extra_compiler_args.get(unit) } } + +#[derive(Copy, Clone, Default, Debug)] +pub struct DepKindSet { + pub build: bool, + pub normal: bool, + pub dev: bool, +} + +impl DepKindSet { + pub fn contains(&self, kind: DepKind) -> bool { + match kind { + DepKind::Build => self.build, + DepKind::Normal => self.normal, + DepKind::Development => self.dev, + } + } +} diff --git a/src/cargo/core/compiler/job_queue/job_state.rs b/src/cargo/core/compiler/job_queue/job_state.rs index 06a672b611d..ee7b710dd39 100644 --- a/src/cargo/core/compiler/job_queue/job_state.rs +++ b/src/cargo/core/compiler/job_queue/job_state.rs @@ -212,4 +212,13 @@ impl<'a, 'gctx> JobState<'a, 'gctx> { self.messages .push(Message::FutureIncompatReport(self.id, report)); } + + /// The rustc emitted the list of unused `--extern` args. + /// + /// This is useful for checking unused dependencies. + /// Should only be called once, as the compiler only emits it once per compilation. + pub fn unused_externs(&self, unused_externs: Vec) { + self.messages + .push(Message::UnusedExterns(self.id, unused_externs)); + } } diff --git a/src/cargo/core/compiler/job_queue/mod.rs b/src/cargo/core/compiler/job_queue/mod.rs index 3a5c150bf08..95f90112980 100644 --- a/src/cargo/core/compiler/job_queue/mod.rs +++ b/src/cargo/core/compiler/job_queue/mod.rs @@ -138,6 +138,7 @@ use super::UnitIndex; use super::custom_build::Severity; use super::timings::SectionTiming; use super::timings::Timings; +use super::unused_deps::UnusedDepState; use crate::core::compiler::descriptive_pkg_name; use crate::core::compiler::future_incompat::{ self, FutureBreakageItem, FutureIncompatReportPackage, @@ -186,6 +187,7 @@ struct DrainState<'gctx> { progress: Progress<'gctx>, next_id: u32, timings: Timings<'gctx>, + unused_dep_state: UnusedDepState, /// Map from unit index to unit, for looking up dependency information. index_to_unit: HashMap, @@ -384,6 +386,7 @@ enum Message { Finish(JobId, Artifact, CargoResult<()>), FutureIncompatReport(JobId, Vec), SectionTiming(JobId, SectionTiming), + UnusedExterns(JobId, Vec), } impl<'gctx> JobQueue<'gctx> { @@ -503,6 +506,7 @@ impl<'gctx> JobQueue<'gctx> { progress, next_id: 0, timings: self.timings, + unused_dep_state: UnusedDepState::new(build_runner), index_to_unit: build_runner .bcx .unit_to_index @@ -543,12 +547,10 @@ impl<'gctx> JobQueue<'gctx> { .take() .map(move |srv| srv.start(move |msg| messages.push(Message::FixDiagnostic(msg)))); - thread::scope( - move |scope| match state.drain_the_queue(build_runner, scope, &helper) { - Some(err) => Err(err), - None => Ok(()), - }, - ) + thread::scope(move |scope| { + let (result,) = state.drain_the_queue(build_runner, scope, &helper); + result + }) } } @@ -722,6 +724,11 @@ impl<'gctx> DrainState<'gctx> { items, }); } + Message::UnusedExterns(id, unused_externs) => { + let unit = &self.active[&id]; + self.unused_dep_state + .record_unused_externs_for_unit(unit, unused_externs); + } Message::Token(acquired_token) => { let token = acquired_token.context("failed to acquire jobserver token")?; self.tokens.push(token); @@ -762,14 +769,15 @@ impl<'gctx> DrainState<'gctx> { /// This is the "main" loop, where Cargo does all work to run the /// compiler. /// - /// This returns an Option to prevent the use of `?` on `Result` types - /// because it is important for the loop to carefully handle errors. + /// This returns a tuple of `Result` to prevent the use of `?` on + /// `Result` types because it is important for the loop to + /// carefully handle errors. fn drain_the_queue<'s>( mut self, build_runner: &mut BuildRunner<'_, '_>, scope: &'s Scope<'s, '_>, jobserver_helper: &HelperThread, - ) -> Option { + ) -> (Result<(), anyhow::Error>,) { trace!("queue: {:#?}", self.queue); // Iteratively execute the entire dependency graph. Each turn of the @@ -813,6 +821,18 @@ impl<'gctx> DrainState<'gctx> { } self.progress.clear(); + if build_runner.bcx.gctx.cli_unstable().cargo_lints { + let mut warn_count = 0; + let mut error_count = 0; + drop(self.unused_dep_state.emit_unused_warnings( + &mut warn_count, + &mut error_count, + build_runner, + )); + errors.count += error_count; + build_runner.compilation.lint_warning_count += warn_count; + } + let profile_name = build_runner.bcx.build_config.requested_profile; // NOTE: this may be a bit inaccurate, since this may not display the // profile for what was actually built. Profile overrides can change @@ -853,7 +873,7 @@ impl<'gctx> DrainState<'gctx> { if let Some(error) = errors.to_error() { // Any errors up to this point have already been printed via the // `display_error` inside `handle_error`. - Some(anyhow::Error::new(AlreadyPrintedError::new(error))) + (Err(anyhow::Error::new(AlreadyPrintedError::new(error))),) } else if self.queue.is_empty() && self.pending_queue.is_empty() { let profile_link = build_runner.bcx.gctx.shell().err_hyperlink( "https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles", @@ -868,10 +888,10 @@ impl<'gctx> DrainState<'gctx> { &self.per_package_future_incompat_reports, ); - None + (Ok(()),) } else { debug!("queue: {:#?}", self.queue); - Some(internal("finished with jobs still left in the queue")) + (Err(internal("finished with jobs still left in the queue")),) } } diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 34cab04b167..acc32e74ae8 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -51,6 +51,7 @@ pub mod timings; mod unit; pub mod unit_dependencies; pub mod unit_graph; +mod unused_deps; use std::borrow::Cow; use std::cell::OnceCell; @@ -74,6 +75,7 @@ use tracing::{debug, instrument, trace}; pub use self::build_config::UserIntent; pub use self::build_config::{BuildConfig, CompileMode, MessageFormat}; pub use self::build_context::BuildContext; +pub use self::build_context::DepKindSet; pub use self::build_context::FileFlavor; pub use self::build_context::FileType; pub use self::build_context::RustcTargetData; @@ -836,6 +838,12 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult base.env("CARGO_TARGET_TMPDIR", tmp.display().to_string()); } + if build_runner.bcx.gctx.cli_unstable().cargo_lints { + // Added last to reduce the risk of RUSTFLAGS or `[lints]` from interfering with + // `unused_dependencies` tracking + base.arg("-Wunused_crate_dependencies"); + } + Ok(base) } @@ -1178,8 +1186,11 @@ fn add_error_format_and_color(build_runner: &BuildRunner<'_, '_>, cmd: &mut Proc } cmd.arg("--error-format=json"); - let mut json = String::from("--json=diagnostic-rendered-ansi,artifacts,future-incompat"); + let mut json = String::from("--json=diagnostic-rendered-ansi,artifacts,future-incompat"); + if build_runner.bcx.gctx.cli_unstable().cargo_lints { + json.push_str(",unused-externs-silent"); + } if let MessageFormat::Short | MessageFormat::Json { short: true, .. } = build_runner.bcx.build_config.message_format { @@ -1189,11 +1200,9 @@ fn add_error_format_and_color(build_runner: &BuildRunner<'_, '_>, cmd: &mut Proc { json.push_str(",diagnostic-unicode"); } - if enable_timings { json.push_str(",timings"); } - cmd.arg(json); let gctx = build_runner.bcx.gctx; @@ -2313,6 +2322,19 @@ fn on_stderr_line_inner( return Ok(false); } + #[derive(serde::Deserialize)] + struct UnusedExterns { + unused_extern_names: Vec, + } + if let Ok(uext) = serde_json::from_str::(compiler_message.get()) { + trace!( + "obtained unused externs list from rustc: `{:?}`", + uext.unused_extern_names + ); + state.unused_externs(uext.unused_extern_names); + return Ok(true); + } + // And failing all that above we should have a legitimate JSON diagnostic // from the compiler, so wrap it in an external Cargo JSON message // indicating which package it came from and then emit it. diff --git a/src/cargo/core/compiler/unit_dependencies.rs b/src/cargo/core/compiler/unit_dependencies.rs index 573d10a40b8..1c11e6ac2c3 100644 --- a/src/cargo/core/compiler/unit_dependencies.rs +++ b/src/cargo/core/compiler/unit_dependencies.rs @@ -35,6 +35,7 @@ use crate::core::{ }; use crate::ops::resolve_all_features; use crate::util::GlobalContext; +use crate::util::Unhashed; use crate::util::interning::InternedString; const IS_NO_ARTIFACT_DEP: Option<&'static Artifact> = None; @@ -206,6 +207,8 @@ fn attach_std_deps( // TODO: Does this `public` make sense? public: true, noprelude: true, + // Artificial dependency + manifest_deps: Unhashed(None), })); found = true; } @@ -305,6 +308,8 @@ fn compute_deps( let mode = check_or_build_mode(unit.mode, dep_lib); let dep_unit_for = unit_for.with_dependency(unit, dep_lib, unit_for.root_compile_kind()); + let manifest_deps = deps.iter().map(|d| (*d).clone()).collect::>(); + let start = ret.len(); if state.gctx.cli_unstable().dual_proc_macros && dep_lib.proc_macro() @@ -315,6 +320,7 @@ fn compute_deps( unit, dep_pkg, dep_lib, + Some(manifest_deps.clone()), dep_unit_for, unit.kind, mode, @@ -326,6 +332,7 @@ fn compute_deps( unit, dep_pkg, dep_lib, + Some(manifest_deps), dep_unit_for, CompileKind::Host, mode, @@ -338,6 +345,7 @@ fn compute_deps( unit, dep_pkg, dep_lib, + Some(manifest_deps), dep_unit_for, unit.kind.for_target(dep_lib), mode, @@ -416,6 +424,7 @@ fn compute_deps( unit, &unit.pkg, t, + None, // artificial UnitFor::new_normal(unit_for.root_compile_kind()), unit.kind.for_target(t), CompileMode::Build, @@ -511,6 +520,7 @@ fn compute_deps_custom_build( unit, &unit.pkg, &unit.target, + None, // artificial script_unit_for, // Build scripts always compiled for the host. CompileKind::Host, @@ -604,6 +614,7 @@ fn artifact_targets_to_unit_deps( target .clone() .set_kind(TargetKind::Lib(vec![target_kind.clone()])), + None, // TBD parent_unit_for, compile_kind, CompileMode::Build, @@ -616,6 +627,7 @@ fn artifact_targets_to_unit_deps( parent, artifact_pkg, target, + None, // TBD parent_unit_for, compile_kind, CompileMode::Build, @@ -651,6 +663,7 @@ fn compute_deps_doc( unit, dep_pkg, dep_lib, + None, // not checking unused deps dep_unit_for, unit.kind.for_target(dep_lib), mode, @@ -664,6 +677,7 @@ fn compute_deps_doc( unit, dep_pkg, dep_lib, + None, // not checking unused deps dep_unit_for, unit.kind.for_target(dep_lib), unit.mode, @@ -697,6 +711,7 @@ fn compute_deps_doc( unit, &unit.pkg, lib, + None, // not checking unused deps dep_unit_for, unit.kind.for_target(lib), unit.mode, @@ -716,6 +731,7 @@ fn compute_deps_doc( scrape_unit, &scrape_unit.pkg, &scrape_unit.target, + None, // not checking unused deps scrape_unit_for, scrape_unit.kind, scrape_unit.mode, @@ -744,6 +760,7 @@ fn maybe_lib( unit, &unit.pkg, t, + None, dep_unit_for, unit.kind.for_target(t), mode, @@ -805,6 +822,7 @@ fn dep_build_script( unit, &unit.pkg, t, + None, // artificial script_unit_for, unit.kind, CompileMode::RunCustomBuild, @@ -841,6 +859,7 @@ fn new_unit_dep( parent: &Unit, pkg: &Package, target: &Target, + manifest_deps: Option>, unit_for: UnitFor, kind: CompileKind, mode: CompileMode, @@ -855,7 +874,16 @@ fn new_unit_dep( kind, ); new_unit_dep_with_profile( - state, parent, pkg, target, unit_for, kind, mode, profile, artifact, + state, + parent, + pkg, + target, + manifest_deps, + unit_for, + kind, + mode, + profile, + artifact, ) } @@ -864,6 +892,7 @@ fn new_unit_dep_with_profile( parent: &Unit, pkg: &Package, target: &Target, + manifest_deps: Option>, unit_for: UnitFor, kind: CompileKind, mode: CompileMode, @@ -911,6 +940,7 @@ fn new_unit_dep_with_profile( dep_name, public, noprelude: false, + manifest_deps: Unhashed(manifest_deps), }) } diff --git a/src/cargo/core/compiler/unit_graph.rs b/src/cargo/core/compiler/unit_graph.rs index 40e2d660e52..bbaaeca9559 100644 --- a/src/cargo/core/compiler/unit_graph.rs +++ b/src/cargo/core/compiler/unit_graph.rs @@ -8,8 +8,10 @@ use crate::GlobalContext; use crate::core::Target; use crate::core::compiler::Unit; use crate::core::compiler::{CompileKind, CompileMode}; +use crate::core::dependency::Dependency; use crate::core::profiles::{Profile, UnitFor}; use crate::util::CargoResult; +use crate::util::Unhashed; use crate::util::interning::InternedString; use std::collections::HashMap; @@ -40,6 +42,10 @@ pub struct UnitDep { pub public: bool, /// If `true`, the dependency should not be added to Rust's prelude. pub noprelude: bool, + /// The manifest dependency that gave rise to this dependency + /// + /// Skip hashing as this is redundant and for book keekping purposes only + pub manifest_deps: Unhashed>>, } const VERSION: u32 = 1; diff --git a/src/cargo/core/compiler/unused_deps.rs b/src/cargo/core/compiler/unused_deps.rs new file mode 100644 index 00000000000..7192e7462d2 --- /dev/null +++ b/src/cargo/core/compiler/unused_deps.rs @@ -0,0 +1,297 @@ +use annotate_snippets::AnnotationKind; +use annotate_snippets::Group; +use annotate_snippets::Level; +use annotate_snippets::Origin; +use annotate_snippets::Patch; +use annotate_snippets::Snippet; +use cargo_util_schemas::manifest; +use indexmap::IndexMap; +use indexmap::IndexSet; +use tracing::trace; + +use super::BuildRunner; +use super::unit::Unit; +use crate::core::Dependency; +use crate::core::Package; +use crate::core::PackageId; +use crate::core::compiler::build_config::CompileMode; +use crate::core::dependency::DepKind; +use crate::core::manifest::TargetKind; +use crate::lints::LintLevel; +use crate::lints::get_key_value_span; +use crate::lints::rel_cwd_manifest_path; +use crate::lints::rules::unused_dependencies::LINT; +use crate::util::errors::CargoResult; +use crate::util::interning::InternedString; + +/// Track and translate `unused_externs` to `unused_dependencies` +pub struct UnusedDepState { + states: IndexMap>, +} + +impl UnusedDepState { + pub fn new(build_runner: &mut BuildRunner<'_, '_>) -> Self { + let mut states = IndexMap::<_, IndexMap<_, DependenciesState>>::new(); + + let roots = &build_runner.bcx.roots; + + // Find all units for a package that can report unused externs + let mut root_build_script_builds = IndexSet::new(); + for root in roots.iter() { + for build_script_run in build_runner.unit_deps(root).iter() { + if !build_script_run.unit.target.is_custom_build() + && build_script_run.unit.pkg.package_id() != root.pkg.package_id() + { + continue; + } + for build_script_build in build_runner.unit_deps(&build_script_run.unit).iter() { + if !build_script_build.unit.target.is_custom_build() + && build_script_build.unit.pkg.package_id() != root.pkg.package_id() + { + continue; + } + if build_script_build.unit.mode != CompileMode::Build { + continue; + } + root_build_script_builds.insert(build_script_build.unit.clone()); + } + } + } + + trace!( + "selected dep kinds: {:?}", + build_runner.bcx.selected_dep_kinds + ); + for root in roots.iter().chain(root_build_script_builds.iter()) { + let pkg_id = root.pkg.package_id(); + let dep_kind = dep_kind_of(root); + if !build_runner.bcx.selected_dep_kinds.contains(dep_kind) { + trace!( + "pkg {} v{} ({dep_kind:?}): ignoring unused deps due to non-exhaustive units", + pkg_id.name(), + pkg_id.version(), + ); + continue; + } + trace!( + "tracking root {} {} ({:?})", + root.pkg.name(), + unit_desc(root), + dep_kind + ); + + let state = states + .entry(pkg_id) + .or_default() + .entry(dep_kind) + .or_default(); + state.needed_units += 1; + for dep in build_runner.unit_deps(root).iter() { + trace!( + " => {} (deps={})", + dep.unit.pkg.name(), + dep.manifest_deps.0.is_some() + ); + let manifest_deps = if let Some(manifest_deps) = &dep.manifest_deps.0 { + Some(manifest_deps.clone()) + } else if dep.unit.pkg.package_id() == root.pkg.package_id() { + None + } else { + continue; + }; + state.externs.insert(dep.extern_crate_name, manifest_deps); + } + } + + Self { states } + } + + pub fn record_unused_externs_for_unit(&mut self, unit: &Unit, unused_externs: Vec) { + let pkg_id = unit.pkg.package_id(); + let kind = dep_kind_of(unit); + if let Some(state) = self.states.get_mut(&pkg_id).and_then(|s| s.get_mut(&kind)) { + state + .unused_externs + .entry(unit.clone()) + .or_default() + .extend(unused_externs.into_iter().map(|s| InternedString::new(&s))); + } + } + + pub fn emit_unused_warnings( + &self, + warn_count: &mut usize, + error_count: &mut usize, + build_runner: &mut BuildRunner<'_, '_>, + ) -> CargoResult<()> { + for (pkg_id, states) in &self.states { + let Some(pkg) = self.get_package(pkg_id) else { + continue; + }; + let toml_lints = pkg + .manifest() + .normalized_toml() + .lints + .clone() + .map(|lints| lints.lints) + .unwrap_or(manifest::TomlLints::default()); + let cargo_lints = toml_lints + .get("cargo") + .cloned() + .unwrap_or(manifest::TomlToolLints::default()); + let (lint_level, reason) = LINT.level( + &cargo_lints, + pkg.manifest().edition(), + pkg.manifest().unstable_features(), + ); + + if lint_level == LintLevel::Allow { + continue; + } + + let manifest_path = rel_cwd_manifest_path(pkg.manifest_path(), build_runner.bcx.gctx); + let mut lint_count = 0; + for (dep_kind, state) in states.iter() { + if state.unused_externs.len() != state.needed_units { + // Some compilations errored without printing the unused externs. + // Don't print the warning in order to reduce false positive + // spam during errors. + trace!( + "pkg {} v{} ({dep_kind:?}): ignoring unused deps due to {} outstanding units", + pkg_id.name(), + pkg_id.version(), + state.needed_units + ); + continue; + } + + for (ext, dependency) in &state.externs { + if state + .unused_externs + .values() + .any(|unused| !unused.contains(ext)) + { + trace!( + "pkg {} v{} ({dep_kind:?}): extern {} is used", + pkg_id.name(), + pkg_id.version(), + ext + ); + continue; + } + + // Implicitly added dependencies (in the same crate) aren't interesting + let dependency = if let Some(dependency) = dependency { + dependency + } else { + continue; + }; + for dependency in dependency { + let manifest = pkg.manifest(); + let document = manifest.document(); + let contents = manifest.contents(); + let level = lint_level.to_diagnostic_level(); + let emitted_source = LINT.emitted_source(lint_level, reason); + let toml_path = dependency.toml_path(); + + let mut primary = Group::with_title(level.primary_title(LINT.desc)); + if let Some(document) = document + && let Some(contents) = contents + && let Some(span) = get_key_value_span(document, &toml_path) + { + let span = span.key.start..span.value.end; + primary = primary.element( + Snippet::source(contents) + .path(&manifest_path) + .annotation(AnnotationKind::Primary.span(span)), + ); + } else { + primary = primary.element(Origin::path(&manifest_path)); + } + if lint_count == 0 { + primary = primary.element(Level::NOTE.message(emitted_source)); + } + lint_count += 1; + let mut report = vec![primary]; + if let Some(document) = document + && let Some(contents) = contents + && let Some(span) = get_key_value_span(document, &toml_path) + { + let span = span.key.start..span.value.end; + let mut help = Group::with_title( + Level::HELP.secondary_title("remove the dependency"), + ); + help = help.element( + Snippet::source(contents) + .path(&manifest_path) + .patch(Patch::new(span, "")), + ); + report.push(help); + } + + if lint_level.is_warn() { + *warn_count += 1; + } + if lint_level.is_error() { + *error_count += 1; + } + build_runner + .bcx + .gctx + .shell() + .print_report(&report, lint_level.force())?; + } + } + } + } + Ok(()) + } + + fn get_package(&self, pkg_id: &PackageId) -> Option<&Package> { + let state = self.states.get(pkg_id)?; + let mut iter = state.values(); + let state = iter.next()?; + let mut iter = state.unused_externs.keys(); + let unit = iter.next()?; + Some(&unit.pkg) + } +} + +/// Track a package's [`DepKind`] +#[derive(Default)] +struct DependenciesState { + /// All declared dependencies + externs: IndexMap>>, + /// Expected [`Self::unused_externs`] entries to know we've received them all + /// + /// To avoid warning in cases where we didn't, + /// e.g. if a [`Unit`] errored and didn't report unused externs. + needed_units: usize, + /// As reported by rustc + unused_externs: IndexMap>, +} + +fn dep_kind_of(unit: &Unit) -> DepKind { + match unit.target.kind() { + TargetKind::Lib(_) => match unit.mode { + // To support lib.rs with #[cfg(test)] use foo_crate as _; + CompileMode::Test => DepKind::Development, + _ => DepKind::Normal, + }, + TargetKind::Bin => DepKind::Normal, + TargetKind::Test => DepKind::Development, + TargetKind::Bench => DepKind::Development, + TargetKind::ExampleLib(_) => DepKind::Development, + TargetKind::ExampleBin => DepKind::Development, + TargetKind::CustomBuild => DepKind::Build, + } +} + +fn unit_desc(unit: &Unit) -> String { + format!( + "{}/{}+{:?}", + unit.target.name(), + unit.target.kind().description(), + unit.mode, + ) +} diff --git a/src/cargo/core/dependency.rs b/src/cargo/core/dependency.rs index db421dd0c24..29853420da9 100644 --- a/src/cargo/core/dependency.rs +++ b/src/cargo/core/dependency.rs @@ -293,6 +293,18 @@ impl Dependency { self.inner.explicit_name_in_toml } + /// The keys to this entry in a `Cargo.toml` file + pub fn toml_path(&self) -> Vec { + let mut path = Vec::new(); + if let Some(platform) = self.platform() { + path.push("target".to_owned()); + path.push(platform.to_string()); + } + path.push(self.kind().kind_table().to_owned()); + path.push((&*self.name_in_toml()).to_owned()); + path + } + pub fn set_kind(&mut self, kind: DepKind) -> &mut Dependency { if self.is_public() { // Setting 'public' only makes sense for normal dependencies diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index bdf06d60757..54bb6ad6e74 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -34,6 +34,7 @@ use crate::lints::rules::non_snake_case_features; use crate::lints::rules::non_snake_case_packages; use crate::lints::rules::redundant_homepage; use crate::lints::rules::redundant_readme; +use crate::lints::rules::unused_build_dependencies_no_build_rs; use crate::lints::rules::unused_workspace_dependencies; use crate::lints::rules::unused_workspace_package_fields; use crate::ops; @@ -1383,6 +1384,13 @@ impl<'gctx> Workspace<'gctx> { )?; non_kebab_case_features(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?; non_snake_case_features(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?; + unused_build_dependencies_no_build_rs( + pkg, + &path, + &cargo_lints, + &mut run_error_count, + self.gctx, + )?; redundant_readme(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?; redundant_homepage(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?; missing_lints_inheritance( diff --git a/src/cargo/lints/mod.rs b/src/cargo/lints/mod.rs index 88db968e2bc..a6df128abf7 100644 --- a/src/cargo/lints/mod.rs +++ b/src/cargo/lints/mod.rs @@ -252,6 +252,12 @@ impl AsIndex for &str { } } +impl AsIndex for String { + fn as_index<'i>(&'i self) -> TomlIndex<'i> { + TomlIndex::Key(self.as_str()) + } +} + impl AsIndex for usize { fn as_index<'i>(&'i self) -> TomlIndex<'i> { TomlIndex::Offset(*self) @@ -448,7 +454,7 @@ impl Lint { (l, r) } - fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String { + pub fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String { format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,) } } @@ -473,6 +479,10 @@ impl Display for LintLevel { } impl LintLevel { + pub fn is_warn(&self) -> bool { + self == &LintLevel::Warn + } + pub fn is_error(&self) -> bool { self == &LintLevel::Forbid || self == &LintLevel::Deny } @@ -486,7 +496,7 @@ impl LintLevel { } } - fn force(self) -> bool { + pub fn force(self) -> bool { match self { Self::Allow => false, Self::Warn => true, diff --git a/src/cargo/lints/rules/mod.rs b/src/cargo/lints/rules/mod.rs index f0f7c684dd4..6b4bc797125 100644 --- a/src/cargo/lints/rules/mod.rs +++ b/src/cargo/lints/rules/mod.rs @@ -10,6 +10,7 @@ mod non_snake_case_packages; mod redundant_homepage; mod redundant_readme; mod unknown_lints; +pub mod unused_dependencies; mod unused_workspace_dependencies; mod unused_workspace_package_fields; @@ -26,6 +27,7 @@ pub use non_snake_case_packages::non_snake_case_packages; pub use redundant_homepage::redundant_homepage; pub use redundant_readme::redundant_readme; pub use unknown_lints::output_unknown_lints; +pub use unused_dependencies::unused_build_dependencies_no_build_rs; pub use unused_workspace_dependencies::unused_workspace_dependencies; pub use unused_workspace_package_fields::unused_workspace_package_fields; @@ -42,6 +44,7 @@ pub static LINTS: &[&crate::lints::Lint] = &[ redundant_homepage::LINT, redundant_readme::LINT, unknown_lints::LINT, + unused_dependencies::LINT, unused_workspace_dependencies::LINT, unused_workspace_package_fields::LINT, ]; diff --git a/src/cargo/lints/rules/unused_dependencies.rs b/src/cargo/lints/rules/unused_dependencies.rs new file mode 100644 index 00000000000..cf3e20c14af --- /dev/null +++ b/src/cargo/lints/rules/unused_dependencies.rs @@ -0,0 +1,158 @@ +use std::path::Path; + +use annotate_snippets::AnnotationKind; +use annotate_snippets::Group; +use annotate_snippets::Level; +use annotate_snippets::Origin; +use annotate_snippets::Patch; +use annotate_snippets::Snippet; +use cargo_util_schemas::manifest::TomlPackageBuild; +use cargo_util_schemas::manifest::TomlToolLints; + +use crate::CargoResult; +use crate::GlobalContext; +use crate::core::Package; +use crate::lints::Lint; +use crate::lints::LintLevel; +use crate::lints::STYLE; +use crate::lints::get_key_value_span; +use crate::lints::rel_cwd_manifest_path; + +pub static LINT: &Lint = &Lint { + name: "unused_dependencies", + desc: "unused dependency", + primary_group: &STYLE, + edition_lint_opts: None, + feature_gate: None, + docs: Some( + r#" +### What it does + +Checks for dependencies that are not used by any of the cargo targets. + +### Why it is bad + +Slows down compilation time. + +### Drawbacks + +The lint is only emitted in specific circumstances as multiple cargo targets exist for the +different dependencies tables and they must all be built to know if a dependency is unused. +Currently, only the selected packages are checked and not all `path` dependencies like most lints. +The cargo target selection flags, +independent of which packages are selected, determine which dependencies tables are checked. +As there is no way to select all cargo targets that use `[dev-dependencies]`, +they are unchecked. + +Examples: +- `cargo check` will lint `[build-dependencies]` and `[dependencies]` +- `cargo check --all-targets` will still only lint `[build-dependencies]` and `[dependencies]` and not `[dev-dependencoes]` +- `cargo check --bin foo` will not lint `[dependencies]` even if `foo` is the only bin though `[build-dependencies]` will be checked +- `cargo check -p foo` will not lint any dependencies tables for the `path` dependency `bar` even if `bar` only has a `[lib]` + +### Example + +```toml +[package] +name = "foo" + +[dependencies] +unused = "1" +``` + +Should be written as: + +```toml +[package] +name = "foo" +``` +"#, + ), +}; + +/// Lint for `[build-dependencies]` without a `build.rs` +/// +/// These are always unused. +/// +/// This must be determined independent of the compiler since there are no build targets to pass to +/// rustc to report on these. +pub fn unused_build_dependencies_no_build_rs( + pkg: &Package, + manifest_path: &Path, + cargo_lints: &TomlToolLints, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let (lint_level, reason) = LINT.level( + cargo_lints, + pkg.manifest().edition(), + pkg.manifest().unstable_features(), + ); + + if lint_level == LintLevel::Allow { + return Ok(()); + } + + let manifest_path = rel_cwd_manifest_path(manifest_path, gctx); + + let manifest = pkg.manifest(); + let Some(package) = &manifest.normalized_toml().package else { + return Ok(()); + }; + if package.build != Some(TomlPackageBuild::Auto(false)) { + return Ok(()); + } + + let document = manifest.document(); + let contents = manifest.contents(); + + for (i, dep_name) in manifest + .normalized_toml() + .build_dependencies() + .iter() + .flat_map(|m| m.keys()) + .enumerate() + { + let level = lint_level.to_diagnostic_level(); + let emitted_source = LINT.emitted_source(lint_level, reason); + + let mut primary = Group::with_title(level.primary_title(LINT.desc)); + if let Some(document) = document + && let Some(contents) = contents + && let Some(span) = get_key_value_span(document, &["build-dependencies", dep_name]) + { + let span = span.key.start..span.value.end; + primary = primary.element( + Snippet::source(contents) + .path(&manifest_path) + .annotation(AnnotationKind::Primary.span(span)), + ); + } else { + primary = primary.element(Origin::path(&manifest_path)); + } + if i == 0 { + primary = primary.element(Level::NOTE.message(emitted_source)); + } + let mut report = vec![primary]; + if let Some(document) = document + && let Some(contents) = contents + && let Some(span) = get_key_value_span(document, &["build-dependencies", dep_name]) + { + let span = span.key.start..span.value.end; + let mut help = Group::with_title(Level::HELP.secondary_title("remove the dependency")); + help = help.element( + Snippet::source(contents) + .path(&manifest_path) + .patch(Patch::new(span, "")), + ); + report.push(help); + } + + if lint_level.is_error() { + *error_count += 1; + } + gctx.shell().print_report(&report, lint_level.force())?; + } + + Ok(()) +} diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs index 64e81fdf3a4..a945d9bef9e 100644 --- a/src/cargo/ops/cargo_compile/mod.rs +++ b/src/cargo/ops/cargo_compile/mod.rs @@ -39,7 +39,6 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use crate::core::compiler::UnitIndex; use crate::core::compiler::UserIntent; use crate::core::compiler::unit_dependencies::build_unit_dependencies; use crate::core::compiler::unit_graph::{self, UnitDep, UnitGraph}; @@ -47,6 +46,7 @@ use crate::core::compiler::{BuildConfig, BuildContext, BuildRunner, Compilation} use crate::core::compiler::{CompileKind, CompileTarget, RustcTargetData, Unit}; use crate::core::compiler::{CrateType, TargetInfo, apply_env_config, standard_lib}; use crate::core::compiler::{DefaultExecutor, Executor, UnitInterner}; +use crate::core::compiler::{DepKindSet, UnitIndex}; use crate::core::profiles::Profiles; use crate::core::resolver::features::{self, CliFeatures, FeaturesFor}; use crate::core::resolver::{ForceAllTargets, HasDevUnits, Resolve}; @@ -423,6 +423,7 @@ pub fn create_bcx<'a, 'gctx>( logger.log(LogMessage::UnitGraphStarted { elapsed }); } + let mut selected_dep_kinds = DepKindSet::default(); for SpecsAndResolvedFeatures { specs, resolved_features, @@ -456,7 +457,9 @@ pub fn create_bcx<'a, 'gctx>( interner, has_dev_units, }; - let mut targeted_root_units = generator.generate_root_units()?; + let (mut targeted_root_units, curr_selected_dep_kinds) = generator.generate_root_units()?; + // Should be fine as the loop iterate is independent of target selection + selected_dep_kinds = curr_selected_dep_kinds; if let Some(args) = target_rustc_crate_types { override_rustc_crate_types(&mut targeted_root_units, args, interner)?; @@ -680,6 +683,7 @@ where `` is the latest version supporting rustc {rustc_version}" logger, pkg_set, build_config, + selected_dep_kinds, profiles, extra_compiler_args, target_data, diff --git a/src/cargo/ops/cargo_compile/unit_generator.rs b/src/cargo/ops/cargo_compile/unit_generator.rs index 85df81c5bc1..c56b3b10124 100644 --- a/src/cargo/ops/cargo_compile/unit_generator.rs +++ b/src/cargo/ops/cargo_compile/unit_generator.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Write; use crate::core::Workspace; +use crate::core::compiler::DepKindSet; use crate::core::compiler::UserIntent; use crate::core::compiler::rustdoc::RustdocScrapeExamples; use crate::core::compiler::unit_dependencies::IsArtifact; @@ -370,13 +371,20 @@ impl<'a> UnitGenerator<'a, '_> { } /// Create a list of proposed targets given the context in `UnitGenerator` - fn create_proposals(&self) -> CargoResult>> { + fn create_proposals(&self) -> CargoResult<(Vec>, DepKindSet)> { let mut proposals: Vec> = Vec::new(); + let mut selected_dep_kinds = DepKindSet::default(); + + selected_dep_kinds.build = true; match *self.filter { CompileFilter::Default { required_features_filterable, } => { + selected_dep_kinds.normal = true; + // can't enable `selected_dep_kinds.dev` because benches aren't enabled for + // UserIntent::Test + for pkg in self.packages { let default = self.filter_default_targets(pkg.targets()); proposals.extend(default.into_iter().map(|target| Proposal { @@ -410,6 +418,14 @@ impl<'a> UnitGenerator<'a, '_> { ref benches, } => { if *lib != LibRule::False { + match bins { + FilterRule::All => { + selected_dep_kinds.normal = true; + } + _ => (), + } + // can't enable `selected_dep_kinds.dev` because doctests aren't enabled + let mut libs = Vec::new(); let compile_mode = to_compile_mode(self.intent); for proposal in self.filter_targets(Target::is_lib, false, compile_mode) { @@ -486,7 +502,7 @@ impl<'a> UnitGenerator<'a, '_> { } } - Ok(proposals) + Ok((proposals, selected_dep_kinds)) } /// Proposes targets from which to scrape examples for documentation @@ -757,9 +773,10 @@ Rustdoc did not scrape the following examples because they require dev-dependenc /// compile. Dependencies for these units are computed later in [`unit_dependencies`]. /// /// [`unit_dependencies`]: crate::core::compiler::unit_dependencies - pub fn generate_root_units(&self) -> CargoResult> { - let proposals = self.create_proposals()?; - self.proposals_to_units(proposals) + pub fn generate_root_units(&self) -> CargoResult<(Vec, DepKindSet)> { + let (proposals, selected_dep_kinds) = self.create_proposals()?; + let units = self.proposals_to_units(proposals)?; + Ok((units, selected_dep_kinds)) } /// Generates units specifically for doc-scraping. diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index 260e719db79..656ecd6ac35 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -24,6 +24,7 @@ pub use self::progress::{Progress, ProgressStyle}; pub use self::queue::Queue; pub use self::rustc::Rustc; pub use self::semver_ext::{OptVersionReq, VersionExt}; +pub use self::unhashed::Unhashed; pub use self::vcs::{FossilRepo, GitRepo, HgRepo, PijulRepo, existing_vcs_repo}; pub use self::workspace::{ add_path_args, path_args, print_available_benches, print_available_binaries, @@ -71,6 +72,7 @@ pub mod sqlite; pub mod style; pub mod toml; pub mod toml_mut; +mod unhashed; mod vcs; mod workspace; diff --git a/src/cargo/util/unhashed.rs b/src/cargo/util/unhashed.rs new file mode 100644 index 00000000000..6e25b9be5a9 --- /dev/null +++ b/src/cargo/util/unhashed.rs @@ -0,0 +1,29 @@ +/// Avoid hashing `T` when included in another type +#[derive(Copy, Clone, Default, Debug)] +pub struct Unhashed(pub T); + +impl std::hash::Hash for Unhashed { + fn hash(&self, _: &mut H) { + // ... + } +} + +impl PartialEq for Unhashed { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl Eq for Unhashed {} + +impl PartialOrd for Unhashed { + fn partial_cmp(&self, other: &Self) -> Option { + Some(other.cmp(&self)) + } +} + +impl Ord for Unhashed { + fn cmp(&self, _: &Self) -> std::cmp::Ordering { + std::cmp::Ordering::Equal + } +} diff --git a/src/doc/src/reference/lints.md b/src/doc/src/reference/lints.md index 7d52c7c529c..ebd652fbadf 100644 --- a/src/doc/src/reference/lints.md +++ b/src/doc/src/reference/lints.md @@ -34,6 +34,7 @@ These lints are all set to the 'warn' level by default. - [`redundant_homepage`](#redundant_homepage) - [`redundant_readme`](#redundant_readme) - [`unknown_lints`](#unknown_lints) +- [`unused_dependencies`](#unused_dependencies) - [`unused_workspace_dependencies`](#unused_workspace_dependencies) - [`unused_workspace_package_fields`](#unused_workspace_package_fields) @@ -404,6 +405,53 @@ this-lint-does-not-exist = "warn" ``` +## `unused_dependencies` +Group: `style` + +Level: `warn` + +### What it does + +Checks for dependencies that are not used by any of the cargo targets. + +### Why it is bad + +Slows down compilation time. + +### Drawbacks + +The lint is only emitted in specific circumstances as multiple cargo targets exist for the +different dependencies tables and they must all be built to know if a dependency is unused. +Currently, only the selected packages are checked and not all `path` dependencies like most lints. +The cargo target selection flags, +independent of which packages are selected, determine which dependencies tables are checked. +As there is no way to select all cargo targets that use `[dev-dependencies]`, +they are unchecked. + +Examples: +- `cargo check` will lint `[build-dependencies]` and `[dependencies]` +- `cargo check --all-targets` will still only lint `[build-dependencies]` and `[dependencies]` and not `[dev-dependencoes]` +- `cargo check --bin foo` will not lint `[dependencies]` even if `foo` is the only bin though `[build-dependencies]` will be checked +- `cargo check -p foo` will not lint any dependencies tables for the `path` dependency `bar` even if `bar` only has a `[lib]` + +### Example + +```toml +[package] +name = "foo" + +[dependencies] +unused = "1" +``` + +Should be written as: + +```toml +[package] +name = "foo" +``` + + ## `unused_workspace_dependencies` Group: `suspicious` diff --git a/tests/testsuite/lints/implicit_minimum_version_req.rs b/tests/testsuite/lints/implicit_minimum_version_req.rs index d074978b6b7..b7605383a9e 100644 --- a/tests/testsuite/lints/implicit_minimum_version_req.rs +++ b/tests/testsuite/lints/implicit_minimum_version_req.rs @@ -50,6 +50,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1" + | ^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -98,6 +109,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1.0" + | ^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -135,6 +157,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.2.3 (registry `dummy-registry`) [CHECKING] dep v1.2.3 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1.0.0" + | ^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1.0.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -183,6 +216,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = { version = "1" } + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = { version = "1" } + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -231,6 +275,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = ">=1.0" + | ^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = ">=1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -268,6 +323,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "<2.0" + | ^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "<2.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -305,6 +371,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1.*" + | ^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1.*" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -342,6 +419,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1.0.*" + | ^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1.0.*" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -379,6 +467,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.1.0 (registry `dummy-registry`) [CHECKING] dep v1.1.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = ">1.0" + | ^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = ">1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -416,6 +515,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "<=2.0" + | ^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "<=2.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -464,6 +574,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = ">=1.0, <2.0" + | ^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = ">=1.0, <2.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -501,6 +622,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "~1.0" + | ^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "~1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -538,6 +670,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "=1" + | ^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "=1" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -580,6 +723,17 @@ edition = "2021" [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOT]/foo/bar) [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | bar = { path = "bar" } + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - bar = { path = "bar" } + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -634,6 +788,17 @@ edition = "2021" [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOT]/foo/bar) [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | bar = { path = "bar", version = "0.1" } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - bar = { path = "bar", version = "0.1" } + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -676,6 +841,17 @@ implicit_minimum_version_req = "warn" [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOTURL]/bar#[..]) [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | bar = { git = '[ROOTURL]/bar' } + | ^^^^^^^^^^^^^^^[..]^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - bar = { git = '[ROOTURL]/bar' } + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -729,6 +905,17 @@ implicit_minimum_version_req = "warn" [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOTURL]/bar#[..]) [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | bar = { git = '[ROOTURL]/bar', version = "0.1" } + | ^^^^^^^^^^^^^^^[..]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - bar = { git = '[ROOTURL]/bar', version = "0.1" } + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -823,6 +1010,17 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [COMPILING] dep v1.0.0 [COMPILING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1.0" + | ^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -872,6 +1070,9 @@ implicit_minimum_version_req = "warn" [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] foo v0.0.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -972,6 +1173,27 @@ implicit_minimum_version_req = "warn" | 8 | regex = "1.0.0" | ++ +[WARNING] unused dependency + --> Cargo.toml:7:1 + | +7 | dep = "1" + | ^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1" + | +[WARNING] unused dependency + --> Cargo.toml:8:1 + | +8 | regex = "1.0" + | ^^^^^^^^^^^^^ + | +[HELP] remove the dependency + | +8 - regex = "1.0" + | "#]]) .run(); @@ -1033,6 +1255,18 @@ workspace = true [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] member v0.0.0 ([ROOT]/foo/member) +[WARNING] unused dependency + --> member/Cargo.toml:7:1 + | +7 | dep.workspace = true + | ^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep.workspace = true +7 + .workspace = true + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) @@ -1183,6 +1417,17 @@ workspace = true [DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) [CHECKING] dep v1.0.0 [CHECKING] member v0.0.0 ([ROOT]/foo/member) +[WARNING] unused dependency + --> member/Cargo.toml:7:1 + | +7 | dep = "1.0" + | ^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +7 - dep = "1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) diff --git a/tests/testsuite/lints/mod.rs b/tests/testsuite/lints/mod.rs index 31698704a75..9518e2ea537 100644 --- a/tests/testsuite/lints/mod.rs +++ b/tests/testsuite/lints/mod.rs @@ -16,6 +16,7 @@ mod non_snake_case_packages; mod redundant_homepage; mod redundant_readme; mod unknown_lints; +mod unused_dependencies; mod unused_workspace_dependencies; mod unused_workspace_package_fields; mod warning; @@ -247,6 +248,17 @@ bar = "0.1.0" [DOWNLOADED] bar v0.1.0 (registry `dummy-registry`) [CHECKING] bar v0.1.0 [CHECKING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:8:1 + | +8 | bar = "0.1.0" + | ^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +8 - bar = "0.1.0" + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) diff --git a/tests/testsuite/lints/unused_dependencies.rs b/tests/testsuite/lints/unused_dependencies.rs new file mode 100644 index 00000000000..4ecaca73bd3 --- /dev/null +++ b/tests/testsuite/lints/unused_dependencies.rs @@ -0,0 +1,1188 @@ +use crate::prelude::*; +use cargo_test_support::project; +use cargo_test_support::registry::Package; +use cargo_test_support::rustc_host; +use cargo_test_support::str; + +#[cargo_test] +fn unused_dep_normal() { + // The most basic case where there is an unused dependency + Package::new("unused", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[CHECKING] unused v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_build() { + Package::new("unused", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [build-dependencies] + unused = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "build.rs", + r#" + fn main() {} + "#, + ) + .file( + "src/main.rs", + r#" + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[COMPILING] unused v0.1.0 +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_build_no_build_rs() { + Package::new("unused", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [build-dependencies] + unused = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_lib_bins() { + // Make sure that dependency uses by both binaries and libraries + // are being registered as used + Package::new("unused", "0.1.0").publish(); + Package::new("lib_used", "0.1.0").publish(); + Package::new("bins_used", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = "0.1.0" + lib_used = "0.1.0" + bins_used = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/lib.rs", + r#" + use lib_used as _; + "#, + ) + .file( + "src/bin/foo.rs", + r#" + use bins_used as _; + fn main() {} + "#, + ) + .file( + "src/bin/bar.rs", + r#" + use bins_used as _; + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 3 packages to latest compatible versions +[DOWNLOADING] crates ... +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] lib_used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] bins_used v0.1.0 (registry `dummy-registry`) +[CHECKING] bins_used v0.1.0 +[CHECKING] unused v0.1.0 +[CHECKING] lib_used v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | + +"#]] + .unordered(), + ) + .run(); + + p.cargo("check -Zcargo-lints --lib") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -Zcargo-lints --bins") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -Zcargo-lints --bin foo") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_build_with_used_dep_normal() { + // Check sharing of a dependency + // between build and proper deps + Package::new("unused_build", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [build-dependencies] + unused_build = "0.1.0" + + [dependencies] + unused_build = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "build.rs", + r#" + fn main() {} + "#, + ) + .file( + "src/main.rs", + r#" + use unused_build as _; + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] unused_build v0.1.0 (registry `dummy-registry`) +[COMPILING] unused_build v0.1.0 +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused_build = "0.1.0" + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused_build = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_normal_but_implicit_used_dep_dev() { + Package::new("used_dev", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + used_dev = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() {} + "#, + ) + .file( + "tests/foo.rs", + r#" + #[test] + fn foo { + use used_dev as _; + } + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] used_dev v0.1.0 (registry `dummy-registry`) +[CHECKING] used_dev v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | used_dev = "0.1.0" + | ^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - used_dev = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_normal_but_explicit_used_dep_dev() { + Package::new("used_once", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + used_once = "0.1.0" + + [dev-dependencies] + used_once = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() {} + "#, + ) + .file( + "tests/foo.rs", + r#" + #[test] + fn foo { + use used_once as _; + } + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] used_once v0.1.0 (registry `dummy-registry`) +[CHECKING] used_once v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | used_once = "0.1.0" + | ^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - used_once = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_dev_but_explicit_used_dep_normal() { + Package::new("used_once", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + used_once = "0.1.0" + + [dev-dependencies] + used_once = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { + use used_once as _; + } + "#, + ) + .file( + "tests/foo.rs", + r#" + #[test] + fn foo { + } + "#, + ) + .build(); + + // No warning because the dependency is used at test time by the unit tests and the two + // dependencies are indistinguishable + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] used_once v0.1.0 (registry `dummy-registry`) +[CHECKING] used_once v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn optional_dependency() { + // The most basic case where there is an unused dependency + Package::new("unused", "0.1.0").publish(); + Package::new("used", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = { version = "0.1.0", optional = true } + used = { version = "0.1.0", optional = true } + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + #[cfg(feature = "used")] + use used as _; + + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -Zcargo-lints -F used,unused") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[DOWNLOADING] crates ... +[DOWNLOADED] used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[CHECKING] unused v0.1.0 +[CHECKING] used v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = { version = "0.1.0", optional = true } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = { version = "0.1.0", optional = true } + | + +"#]] + .unordered(), + ) + .run(); +} + +#[cargo_test] +fn unused_dep_renamed() { + // Make sure that package renaming works + Package::new("bar", "0.1.0").publish(); + Package::new("baz", "0.2.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + baz = { package = "bar", version = "0.1.0" } + bar = { package = "baz", version = "0.2.0" } + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + use bar as _; + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[DOWNLOADING] crates ... +[DOWNLOADED] baz v0.2.0 (registry `dummy-registry`) +[DOWNLOADED] bar v0.1.0 (registry `dummy-registry`) +[CHECKING] baz v0.2.0 +[CHECKING] bar v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | baz = { package = "bar", version = "0.1.0" } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - baz = { package = "bar", version = "0.1.0" } + | + +"#]] + .unordered(), + ) + .run(); +} + +#[cargo_test] +fn warning_replay() { + // The most basic case where there is an unused dependency + Package::new("unused", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() {} + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[CHECKING] unused v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_dep_target() { + // The most basic case where there is an unused dependency + Package::new("unused", "0.1.0").publish(); + Package::new("used", "0.1.0").publish(); + let host = rustc_host(); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [target.{host}.dependencies] + unused = "0.1.0" + used = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "# + ), + ) + .file( + "src/main.rs", + r#" + use used as _; + fn main() {} + "#, + ) + .build(); + + p.cargo(&format!("check -Zcargo-lints --target {host}")) + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[DOWNLOADING] crates ... +[DOWNLOADED] used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[CHECKING] unused v0.1.0 +[CHECKING] used v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[WARNING] unused dependency + --> Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | + +"#]] + .unordered(), + ) + .run(); +} + +#[cargo_test] +fn unused_dev_deps() { + // Test for unused dev dependencies + Package::new("unit_used", "0.1.0").publish(); + Package::new("doctest_used", "0.1.0").publish(); + Package::new("test_used", "0.1.0").publish(); + Package::new("example_used", "0.1.0").publish(); + Package::new("bench_used", "0.1.0").publish(); + Package::new("unused", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dev-dependencies] + unit_used = "0.1.0" + doctest_used = "0.1.0" + test_used = "0.1.0" + example_used = "0.1.0" + bench_used = "0.1.0" + unused = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "src/lib.rs", + r#" + /// ``` + /// use doctest_used as _; + /// ``` + pub fn foo() {} + + #[test] + fn test() { + use unit_used as _; + } + "#, + ) + .file( + "tests/hello.rs", + r#" + use test_used as _; + "#, + ) + .file( + "examples/hello.rs", + r#" + use example_used as _; + fn main() {} + "#, + ) + .file( + "benches/hello.rs", + r#" + use bench_used as _; + fn main() {} + "#, + ) + .build(); + + // doesn't check any tests, still no unused dev dep warnings + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 6 packages to latest compatible versions +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + // doesn't check doctests, still no unused dev dep warnings + p.cargo("check -Zcargo-lints --all-targets") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[DOWNLOADING] crates ... +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unit_used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] test_used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] example_used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] doctest_used v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] bench_used v0.1.0 (registry `dummy-registry`) +[CHECKING] bench_used v0.1.0 +[CHECKING] unused v0.1.0 +[CHECKING] doctest_used v0.1.0 +[CHECKING] example_used v0.1.0 +[CHECKING] unit_used v0.1.0 +[CHECKING] test_used v0.1.0 +[CHECKING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ) + .run(); + + // doesn't test doctests and benches and thus doesn't create unused dev dep warnings + p.cargo("test -Zcargo-lints --no-run") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[COMPILING] example_used v0.1.0 +[COMPILING] unit_used v0.1.0 +[COMPILING] unused v0.1.0 +[COMPILING] test_used v0.1.0 +[COMPILING] bench_used v0.1.0 +[COMPILING] doctest_used v0.1.0 +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `test` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[EXECUTABLE] unittests src/lib.rs (target/debug/deps/foo-[HASH][EXE]) +[EXECUTABLE] tests/hello.rs (target/debug/deps/hello-[HASH][EXE]) + +"#]] + .unordered(), + ) + .run(); + + // doesn't test doctests, still no unused dev dep warnings + p.cargo("test -Zcargo-lints --no-run --all-targets") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[FINISHED] `test` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[EXECUTABLE] unittests src/lib.rs (target/debug/deps/foo-[HASH][EXE]) +[EXECUTABLE] tests/hello.rs (target/debug/deps/hello-[HASH][EXE]) +[EXECUTABLE] benches/hello.rs (target/debug/deps/hello-[HASH][EXE]) +[EXECUTABLE] unittests examples/hello.rs (target/debug/examples/hello-[HASH][EXE]) + +"#]]) + .run(); + + // tests everything including doctests, but not + // the benches + p.cargo("test -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[FINISHED] `test` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[RUNNING] unittests src/lib.rs (target/debug/deps/foo-[HASH][EXE]) +[RUNNING] tests/hello.rs (target/debug/deps/hello-[HASH][EXE]) +[DOCTEST] foo + +"#]]) + .run(); +} + +#[cargo_test] +fn package_selection() { + // Make sure that workspaces are supported, + // --all params, -p params, etc. + 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(); + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["foo", "bar"] + exclude = ["external"] + "#, + ) + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = "0.1.0" + used_foo = "0.1.0" + bar.path = "../bar" + external.path = "../external" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "foo/src/lib.rs", + r#" + use used_foo as _; + "#, + ) + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = "0.1.0" + used_bar = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "bar/src/lib.rs", + r#" + use used_bar as _; + "#, + ) + .file( + "external/Cargo.toml", + r#" + [package] + name = "external" + version = "0.1.0" + authors = [] + edition = "2018" + + [dependencies] + unused = "0.1.0" + used_external = "0.1.0" + + [lints.cargo] + unused_dependencies = "warn" + "#, + ) + .file( + "external/src/lib.rs", + r#" + use used_external as _; + "#, + ) + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[UPDATING] `dummy-registry` index +[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] used_bar v0.1.0 (registry `dummy-registry`) +[DOWNLOADED] unused v0.1.0 (registry `dummy-registry`) +[CHECKING] unused v0.1.0 +[CHECKING] used_bar v0.1.0 +[CHECKING] used_external v0.1.0 +[CHECKING] used_foo v0.1.0 +[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" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | +[WARNING] unused dependency + --> foo/Cargo.toml:11:13 + | +11 | bar.path = "../bar" + | ^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +11 - bar.path = "../bar" +11 + .path = "../bar" + | +[WARNING] unused dependency + --> foo/Cargo.toml:12:13 + | +12 | external.path = "../external" + | ^^^^^^^^ + | +[HELP] remove the dependency + | +12 - external.path = "../external" +12 + .path = "../external" + | +[WARNING] unused dependency + --> foo/Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | + +"#]] + .unordered(), + ) + .run(); + + p.cargo("check -Zcargo-lints -p foo") + .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 + | +11 | bar.path = "../bar" + | ^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +11 - bar.path = "../bar" +11 + .path = "../bar" + | +[WARNING] unused dependency + --> foo/Cargo.toml:12:13 + | +12 | external.path = "../external" + | ^^^^^^^^ + | +[HELP] remove the dependency + | +12 - external.path = "../external" +12 + .path = "../external" + | +[WARNING] unused dependency + --> foo/Cargo.toml:9:13 + | +9 | unused = "0.1.0" + | ^^^^^^^^^^^^^^^^ + | +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | + +"#]] + .unordered(), + ) + .run(); + + p.cargo("check -Zcargo-lints -p bar") + .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" + | ^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - unused = "0.1.0" + | + +"#]] + .unordered(), + ) + .run(); +} diff --git a/tests/testsuite/lints/unused_workspace_dependencies.rs b/tests/testsuite/lints/unused_workspace_dependencies.rs index 558ffa4cce5..ac57932b207 100644 --- a/tests/testsuite/lints/unused_workspace_dependencies.rs +++ b/tests/testsuite/lints/unused_workspace_dependencies.rs @@ -99,12 +99,36 @@ workspace = true | 11 - unused = "1" | +[WARNING] unused dependency + --> bar/Cargo.toml:9:1 + | +9 | build-dep.workspace = true + | ^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +9 - build-dep.workspace = true +9 + .workspace = true + | [UPDATING] `dummy-registry` index [LOCKING] 6 packages to latest compatible versions [DOWNLOADING] crates ... [DOWNLOADED] in-package v1.0.0 (registry `dummy-registry`) [CHECKING] in-package v1.0.0 [CHECKING] foo v0.0.1 ([ROOT]/foo) +[WARNING] unused dependency + --> Cargo.toml:24:1 + | +24 | in-package.workspace = true + | ^^^^^^^^^^ + | + = [NOTE] `cargo::unused_dependencies` is set to `warn` by default +[HELP] remove the dependency + | +24 - in-package.workspace = true +24 + .workspace = true + | [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]])