diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs index 18eaa16a0a7e..12f9ae8cd139 100644 --- a/src/cargo/core/summary.rs +++ b/src/cargo/core/summary.rs @@ -30,6 +30,49 @@ struct Inner { rust_version: Option, } +// Indicates the dependency inferred from the `dep` syntax that should exist, but missing on the resolved dependencies +pub struct MissingDependency { + dep_name: InternedString, + feature: InternedString, + feature_value: FeatureValue, + weak_optional: bool, // Indicates the dependency inferred from the `dep?` syntax that is weak optional + unused_dependency: bool, // Indicates the dependency is unused but not absent in the manifest +} + +impl MissingDependency { + pub fn set_unused_dependency(&mut self, flag: bool) { + self.unused_dependency = flag + } + pub fn weak_optional(&self) -> bool { + self.weak_optional + } + pub fn dep_name(&self) -> String { + self.dep_name.to_string() + } +} + +impl fmt::Display for MissingDependency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.unused_dependency && self.weak_optional { + write!( + f, + "feature `{feature}` includes `{fv}`, but missing `dep:{dep_name}` to activate it", + feature = &self.feature, + fv = &self.feature_value, + dep_name = &self.dep_name + ) + } else { + write!( + f, + "feature `{feature}` includes `{fv}`, but `{dep_name}` is not a dependency", + feature = &self.feature, + fv = &self.feature_value, + dep_name = &self.dep_name + ) + } + } +} + impl Summary { #[tracing::instrument(skip_all)] pub fn new( @@ -274,7 +317,13 @@ fn build_feature_map( // Validation of the feature name will be performed in the resolver. if !is_any_dep { - bail!("feature `{feature}` includes `{fv}`, but `{dep_name}` is not a dependency"); + bail!(MissingDependency { + feature: *feature, + feature_value: (*fv).clone(), + dep_name: *dep_name, + weak_optional: *weak, + unused_dependency: false, + }) } if *weak && !is_optional_dep { bail!( diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 001b1bc74e40..e94fb2a11306 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::str::{self, FromStr}; +use crate::core::summary::MissingDependency; use crate::AlreadyPrintedError; use anyhow::{anyhow, bail, Context as _}; use cargo_platform::Platform; @@ -1435,24 +1436,53 @@ fn to_real_manifest( .unwrap_or_else(|| semver::Version::new(0, 0, 0)), source_id, ); - let summary = Summary::new( - pkgid, - deps, - &resolved_toml - .features - .as_ref() - .unwrap_or(&Default::default()) - .iter() - .map(|(k, v)| { - ( - InternedString::new(k), - v.iter().map(InternedString::from).collect(), - ) - }) - .collect(), - resolved_package.links.as_deref(), - rust_version.clone(), - )?; + let summary = { + let mut summary = Summary::new( + pkgid, + deps, + &resolved_toml + .features + .as_ref() + .unwrap_or(&Default::default()) + .iter() + .map(|(k, v)| { + ( + InternedString::new(k), + v.iter().map(InternedString::from).collect(), + ) + }) + .collect(), + resolved_package.links.as_deref(), + rust_version.clone(), + ); + // editon2024 stops expose implicit features, which will strip weak optional dependencies from `dependencies`, + // need to check whether `dep_name` is stripped as unused dependency + if let Err(ref mut err) = summary { + if let Some(missing_dep) = err.downcast_mut::() { + if missing_dep.weak_optional() { + // dev-dependencies are not allowed to be optional + let mut orig_deps = vec![ + original_toml.dependencies.as_ref(), + original_toml.build_dependencies.as_ref(), + ]; + for (_, platform) in original_toml.target.iter().flatten() { + orig_deps.extend(vec![ + platform.dependencies.as_ref(), + platform.build_dependencies.as_ref(), + ]); + } + for deps in orig_deps { + if let Some(deps) = deps { + if deps.keys().any(|p| *p.as_str() == missing_dep.dep_name()) { + missing_dep.set_unused_dependency(true); + } + } + } + } + } + } + summary? + }; if summary.features().contains_key("default-features") { warnings.push( "`default-features = [\"..\"]` was found in [features]. \ diff --git a/tests/testsuite/lints/unused_optional_dependencies.rs b/tests/testsuite/lints/unused_optional_dependencies.rs index cf1d64247125..177f4fe61e19 100644 --- a/tests/testsuite/lints/unused_optional_dependencies.rs +++ b/tests/testsuite/lints/unused_optional_dependencies.rs @@ -2,6 +2,7 @@ use cargo_test_support::project; use cargo_test_support::registry::Package; +use cargo_test_support::str; #[cargo_test(nightly, reason = "edition2024 is not stable")] fn default() { @@ -258,14 +259,13 @@ fn inactive_weak_optional_dep() { p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"]) .with_status(101) - .with_stderr( - "\ -error: failed to parse manifest at `[ROOT]/foo/Cargo.toml` + .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` Caused by: feature `foo_feature` includes `dep_name?/dep_feature`, but `dep_name` is not a dependency -", - ) + +"#]]) .run(); // This test is that we need to improve in edition2024, we need to tell that a weak optioanl dependency needs specify @@ -293,13 +293,44 @@ Caused by: p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"]) .with_status(101) - .with_stderr( - "\ -error: failed to parse manifest at `[ROOT]/foo/Cargo.toml` + .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` Caused by: - feature `foo_feature` includes `dep_name?/dep_feature`, but `dep_name` is not a dependency -", + feature `foo_feature` includes `dep_name?/dep_feature`, but missing `dep:dep_name` to activate it + +"#]]) + .run(); + // Check target.'cfg(unix)'.dependencies can work + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["edition2024"] + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + + [target.'cfg(unix)'.dependencies] + dep_name = { version = "0.1.0", optional = true } + + [features] + foo_feature = ["dep_name?/dep_feature"] + "#, ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` + +Caused by: + feature `foo_feature` includes `dep_name?/dep_feature`, but missing `dep:dep_name` to activate it + +"#]]) .run(); }