Repository: rust-lang/cargo (rust)

Issue:
Bad error message when a strong dep features's depedency is an unused optional dependency
### Problem

If nothing can activate an optional dependency, cargo acts as if the dependency doesn't exist which creates poor error messages.

Also, with how things are arranged, the unused optional dependency lint doesn't get reported which could at least reduce the burden on the error message.

### Steps

Baseline `Cargo.toml`:
```toml
cargo-features = ["edition2024"]
[package]
name = "cargo-14010"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1.0.203", optional = true }
```
```console
$ cargo +nightly check -Zcargo-lints
warning: unused optional dependency
 --> Cargo.toml:8:1
  |
8 | serde = { version = "1.0.203", optional = true }
  | -----
  |
  = note: `cargo::unused_optional_dependency` is set to `warn` by default
  = help: remove the dependency or activate it in a feature with `dep:serde`
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
```

Add the following to `Cargo.toml`:
```toml

[features]
serde = ["serde/derive"]
```
```console
$ cargo +nightly check -Zcargo-lints
error: failed to parse manifest at `/home/epage/src/personal/dump/cargo-14010-1/Cargo.toml`

Caused by:
  feature `serde` includes `serde/derive`, but `serde` is not a dependency
```

### Possible Solution(s)

- Improve the error message
- Implicitly create `dep:serde` if `serde/derive` is used
- Deprecate `serde/derive` syntax, removing it in a new edition (#10556)

### Notes

This was split out of #14015 assuming we'll go with a different solution than it

### Version

_No response_

Pull request description:
fix: Ensure dep/feature activates the dependency on 2024 
### What does this PR try to resolve?

Fixes #14016

### How should we test and review this PR?

This doesn't revert the last commit of #14018 so that it works properly
on Editions 2021 and 2024.

### Additional information


Patch:
diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs
index 1d2c23a92..5bd6f79fe 100644
--- a/src/cargo/util/toml/mod.rs
+++ b/src/cargo/util/toml/mod.rs
@@ -291,7 +291,7 @@ fn resolve_toml(
         dev_dependencies2: None,
         build_dependencies: None,
         build_dependencies2: None,
-        features: original_toml.features.clone(),
+        features: None,
         target: None,
         replace: original_toml.replace.clone(),
         patch: original_toml.patch.clone(),
@@ -322,6 +322,8 @@ fn resolve_toml(
             });
         resolved_toml.package = Some(resolved_package);
 
+        resolved_toml.features = resolve_features(original_toml.features.as_ref(), edition)?;
+
         resolved_toml.lib = targets::resolve_lib(
             original_toml.lib.as_ref(),
             package_root,
@@ -686,6 +688,40 @@ fn default_readme_from_package_root(package_root: &Path) -> Option<String> {
     None
 }
 
+#[tracing::instrument(skip_all)]
+fn resolve_features(
+    original_features: Option<&BTreeMap<manifest::FeatureName, Vec<String>>>,
+    edition: Edition,
+) -> CargoResult<Option<BTreeMap<manifest::FeatureName, Vec<String>>>> {
+    let Some(mut resolved_features) = original_features.cloned() else {
+        return Ok(None);
+    };
+
+    if Edition::Edition2024 <= edition {
+        for activations in resolved_features.values_mut() {
+            let mut deps = Vec::new();
+            for feature_value in activations.iter() {
+                let feature_value = FeatureValue::new(InternedString::new(feature_value));
+                let FeatureValue::DepFeature {
+                    dep_name,
+                    dep_feature: _,
+                    weak: false,
+                } = feature_value
+                else {
+                    continue;
+                };
+                let dep = FeatureValue::Dep { dep_name }.to_string();
+                if !activations.contains(&dep) {
+                    deps.push(dep);
+                }
+            }
+            activations.extend(deps);
+        }
+    }
+
+    Ok(Some(resolved_features))
+}
+
 #[tracing::instrument(skip_all)]
 fn resolve_dependencies<'a>(
     gctx: &GlobalContext,


Test patch:
diff --git a/tests/testsuite/features.rs b/tests/testsuite/features.rs
index a7c43132b..1213c4a55 100644
--- a/tests/testsuite/features.rs
+++ b/tests/testsuite/features.rs
@@ -1847,6 +1847,73 @@ fn features_option_given_twice() {
     p.cargo("check --features a --features b").run();
 }
 
+#[cargo_test(nightly, reason = "edition2024 is not stable")]
+fn strong_dep_feature_edition2024() {
+    let p = project()
+        .file(
+            "Cargo.toml",
+            r#"
+                cargo-features = ["edition2024"]
+                [package]
+                name = "foo"
+                version = "0.1.0"
+                edition = "2024"
+
+                [features]
+                optional_dep = ["optional_dep/foo"]
+
+                [dependencies]
+                optional_dep = { path = "optional_dep", optional = true }
+            "#,
+        )
+        .file(
+            "src/main.rs",
+            r#"
+               fn main() {}
+            "#,
+        )
+        .file(
+            "optional_dep/Cargo.toml",
+            r#"
+            [package]
+            name = "optional_dep"
+            [features]
+            foo = []
+"#,
+        )
+        .file(
+            "optional_dep/src/lib.rs",
+            r#"
+"#,
+        )
+        .build();
+
+    p.cargo("metadata")
+        .masquerade_as_nightly_cargo(&["edition2024"])
+        .with_stdout_data(
+            str![[r#"
+{
+  "metadata": null,
+  "packages": [
+    {
+      "features": {
+        "optional_dep": [
+          "optional_dep/foo",
+          "dep:optional_dep"
+        ]
+      },
+      "name": "foo",
+      "...": "{...}"
+    }
+  ],
+  "...": "{...}"
+}
+"#]]
+            .json(),
+        )
+        .run();
+}
+
 #[cargo_test]
 fn multi_multi_features() {
     let p = project()