Repository: frewsxcv/cargo-all-features (language: rust)

Issue:
Optional dependencies that do not create a feature of the same name
Since Rust 1.60, it is possible to add an optional dependency without adding a feature of the same name ([documentation](https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies)). However, `cargo-all-features` expects these features to exist.

Reproduction:
```toml
# normal package declaration

[dependencies]
log = { version = "0.4", optional = true }

[features]
logging = ["dep:log"]
```

```sh
$ cargo test-all-features
     Testing crate=tester features=[]
# ...
     Testing crate=tester features=[logging]
error: Package `tester v0.1.0 (...)` does not have feature `logging`. It has an optional dependency with that name, but that dependency uses the "dep:" syntax in the features table, so it does not have an implicit feature with that name.
```


Discussion / hints:

Fix patch:
diff --git a/src/features_finder.rs b/src/features_finder.rs
index ca0565d..0ef00c9 100644
--- a/src/features_finder.rs
+++ b/src/features_finder.rs
@@ -10,41 +10,46 @@ pub fn fetch_feature_sets(package: &crate::cargo_metadata::Package) -> Vec<Featu
 
     let filter_denylist = |f: &Feature| !package.denylist.contains(f);
 
-    // Pre-Cargo 1.60, `cargo metadata` would only report explicitly specified features
-    // Post-Cargo 1.60, with the advent of dep: syntax (https://doc.rust-lang.org/nightly/cargo/reference/features.html#optional-dependencies),
-    // `cargo metadata` will unconditionally report all features, including ones autoderived from optional dependencies.
-    //
-    // We try to handle both cases by searching for any use of the `dep:` keyword, and treating features that have been marked as optional AND
-    // exist simply as `foo = ["dep:foo"]` as implicit optional dependencies; even if they were explicitly specified. Upstream bug at
-    // https://github.com/rust-lang/cargo/issues/10543
-    let optional_deps: Vec<_> = fetch_optional_dependencies(package)
-        .filter(filter_denylist)
-        .collect();
-    let mut optional_denylist = HashSet::new();
-    let mut found_dep = false;
-    for dep in &optional_deps {
-        if let Some(sub) = package.feature_map.get(&**dep) {
-            if sub.iter().any(|v| v.starts_with("dep:")) {
-                found_dep = true;
-                if sub.len() == 1 && sub[0] == format!("dep:{}", dep.0) {
-                    optional_denylist.insert(dep);
-                }
+    let mut implicit_features = HashSet::<&str>::new();
+    let mut optional_dep_used_with_dep_syntax_outside_of_implicit_feature = HashSet::new();
+
+    for (feature, implied_features) in &package.feature_map {
+        for implied_dep in implied_features
+            .iter()
+            .filter_map(|v| v.strip_prefix("dep:"))
+        {
+            if implied_features.len() == 1 && implied_dep == feature {
+                // Feature of the shape foo = ["dep:foo"]
+                implicit_features.insert(feature);
+            } else {
+                optional_dep_used_with_dep_syntax_outside_of_implicit_feature.insert(implied_dep);
             }
         }
     }
 
+    // If the dep is used with `dep:` syntax in another feature, 
+    // it's an explicit feature, because cargo wouldn't generate
+    // the implicit feature.
+    for x in &optional_dep_used_with_dep_syntax_outside_of_implicit_feature {
+        implicit_features.remove(x);
+    }
+
     if package.allowlist.is_empty() {
-        // This handles the pre-1.60 case
-        if !package.skip_optional_dependencies && !found_dep {
-            features.extend(optional_deps.iter().filter(|f| filter_denylist(f)).cloned());
+        if !package.skip_optional_dependencies {
+            features.extend(
+                fetch_optional_dependencies(package)
+                    .filter(filter_denylist)
+                    .filter(|f: &Feature| {
+                        !optional_dep_used_with_dep_syntax_outside_of_implicit_feature
+                            .contains(f.0.as_str())
+                    }),
+            );
         }
+
         features.extend(
             fetch_features(package)
                 .filter(filter_denylist)
-                // This handles the post-1.60 case
-                .filter(|f: &Feature| {
-                    !package.skip_optional_dependencies || !optional_denylist.contains(f)
-                }),
+                .filter(|f: &Feature| !implicit_features.contains(f.0.as_str())),
         );
 
         features.extend(


Test patch: