Repository: taiki-e/cargo-hack (language: rust)

Issue:
Excluding a feature present on only some crates in a workspace displays warning
Excluding a feature that is not on all crates in a workspace displays warning for each crate that doesn't have the feature.

For example, in the following repo the feature `docs` is defined on the `stellar-contract-sdk` crate, but no others. When excluding the feature a warning displays for all other repos:
```
$ git clone https://github.com/stellar/rs-stellar-contract-sdk
$ cd rs-stellar-contract-sdk
$ git checkout 9481010ac820ce3694d5853db79beee38e302690
$ cargo hack --feature-powerset --exclude-features docs test
warning: specified feature `docs` not found in package `example_add_i32`
warning: specified feature `docs` not found in package `example_add_i64`
warning: specified feature `docs` not found in package `example_udt`
warning: specified feature `docs` not found in package `example_contract_data`
warning: specified feature `docs` not found in package `example_create_contract`
warning: specified feature `docs` not found in package `example_linear_memory`
...
```


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc84f382..84c9ab17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
 
 ## [Unreleased]
 
+- Fix an issue that a warning was displayed when excluding a feature that exists only in some crates in the workspace. ([#158](https://github.com/taiki-e/cargo-hack/pull/158))
+
 ## [0.5.15] - 2022-07-18
 
 - Support namespaced features (features with `dep:` prefix). ([#154](https://github.com/taiki-e/cargo-hack/pull/154))
diff --git a/src/context.rs b/src/context.rs
index b4e0bac0..223c5d38 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -84,7 +84,7 @@ impl Context {
         &self.metadata.packages[id]
     }
 
-    pub(crate) fn workspace_members(&self) -> impl Iterator<Item = &PackageId> {
+    pub(crate) fn workspace_members(&self) -> impl ExactSizeIterator<Item = &PackageId> {
         self.metadata.workspace_members.iter()
     }
 
diff --git a/src/main.rs b/src/main.rs
index 6c271dc3..cbb1ea17 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -131,7 +131,12 @@ enum Kind<'a> {
     Powerset { features: Vec<Vec<&'a Feature>> },
 }
 
-fn determine_kind<'a>(cx: &'a Context, id: &PackageId, progress: &mut Progress) -> Kind<'a> {
+fn determine_kind<'a>(
+    cx: &'a Context,
+    id: &PackageId,
+    progress: &mut Progress,
+    multiple_packages: bool,
+) -> Kind<'a> {
     if cx.ignore_private && cx.is_private(id) {
         info!("skipped running on private package `{}`", cx.name_verbose(id));
         return Kind::SkipAsPrivate;
@@ -151,18 +156,22 @@ fn determine_kind<'a>(cx: &'a Context, id: &PackageId, progress: &mut Progress)
             && !cx.group_features.iter().any(|g| g.matches(f.name()))
     };
     let features = if cx.include_features.is_empty() {
-        cx.exclude_features.iter().for_each(|d| {
-            if !pkg_features.contains(d) {
-                warn!("specified feature `{}` not found in package `{}`", d, package.name);
+        // TODO
+        if !multiple_packages {
+            for name in &cx.exclude_features {
+                if !pkg_features.contains(name) {
+                    warn!("specified feature `{}` not found in package `{}`", name, package.name);
+                }
             }
-        });
+        }
 
         let mut features: Vec<_> = pkg_features.normal().iter().filter(filter).collect();
 
         if let Some(opt_deps) = &cx.optional_deps {
             if opt_deps.len() == 1 && opt_deps[0].is_empty() {
                 // --optional-deps=
-            } else {
+            } else if !multiple_packages {
+                // TODO
                 for d in opt_deps {
                     if !pkg_features.optional_deps().iter().any(|f| f == d) {
                         warn!(
@@ -244,9 +253,10 @@ fn determine_package_list<'a>(
             }
         }
 
+        let multiple_packages = cx.workspace_members().len().saturating_sub(cx.exclude.len()) > 1;
         cx.workspace_members()
             .filter(|id| !cx.exclude.contains(&cx.packages(id).name))
-            .map(|id| (id, determine_kind(cx, id, progress)))
+            .map(|id| (id, determine_kind(cx, id, progress, multiple_packages)))
             .collect()
     } else if !cx.package.is_empty() {
         if let Some(spec) = cx
@@ -257,17 +267,22 @@ fn determine_package_list<'a>(
             bail!("package ID specification `{}` matched no packages", spec)
         }
 
+        let multiple_packages = cx.package.len() > 1;
         cx.workspace_members()
             .filter(|id| cx.package.contains(&cx.packages(id).name))
-            .map(|id| (id, determine_kind(cx, id, progress)))
+            .map(|id| (id, determine_kind(cx, id, progress, multiple_packages)))
             .collect()
     } else if cx.current_package().is_none() {
-        cx.workspace_members().map(|id| (id, determine_kind(cx, id, progress))).collect()
+        let multiple_packages = cx.workspace_members().len() > 1;
+        cx.workspace_members()
+            .map(|id| (id, determine_kind(cx, id, progress, multiple_packages)))
+            .collect()
     } else {
         let current_package = &cx.packages(cx.current_package().unwrap()).name;
+        let multiple_packages = false;
         cx.workspace_members()
             .find(|id| cx.packages(id).name == *current_package)
-            .map(|id| vec![(id, determine_kind(cx, id, progress))])
+            .map(|id| vec![(id, determine_kind(cx, id, progress, multiple_packages))])
             .unwrap_or_default()
     })
 }


Test patch:
diff --git a/tests/test.rs b/tests/test.rs
index ec6f2c15..0fc54395 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -772,6 +772,13 @@ fn include_features() {
         );
 }
 
+#[test]
+fn exclude_features() {
+    cargo_hack(["check", "--each-feature", "--exclude-features", "f"])
+        .assert_success("virtual")
+        .stderr_not_contains("specified feature `f` not found");
+}
+
 #[test]
 fn exclude_features_failure() {
     cargo_hack(["check", "--exclude-features", "a"])