commit d3707a9b2b6ceccf218799136f60672fa380b545
parent 14ad6e0c95da0a362540b3ee443fb60cfc3e60c0
Author: Zack Newman <zack@philomathiclife.com>
Date: Wed, 8 Oct 2025 16:56:01 -0600
fix bugs dealing with feature dependencies
Diffstat:
4 files changed, 77 insertions(+), 43 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -2,7 +2,7 @@
authors = ["Zack Newman <zack@philomathiclife.com>"]
categories = ["command-line-utilities", "development-tools::testing", "rust-patterns"]
description = "Continuous integration for Clippy, unit tests, and doc tests for all possible features."
-documentation = "https://git.philomathiclife.com/ci-cargo/file/README.md.html"
+documentation = "https://crates.io/crates/ci-cargo"
edition = "2024"
keywords = ["cargo", "ci", "rust"]
license = "MIT OR Apache-2.0"
diff --git a/src/args.rs b/src/args.rs
@@ -745,6 +745,7 @@ impl MetaCmd {
#[cfg(test)]
mod tests {
use super::{ArgsErr, Cmd, Ignored, MetaCmd, Opts, OsString, PathBuf};
+ use core::iter;
#[cfg(unix)]
use std::os::unix::ffi::OsStringExt as _;
#[expect(
@@ -752,17 +753,12 @@ mod tests {
clippy::too_many_lines,
reason = "want to test for a lot of things"
)]
- #[expect(
- clippy::iter_on_empty_collections,
- clippy::iter_on_single_items,
- reason = "want to test for it"
- )]
#[test]
fn arg_parsing() {
- assert_eq!(MetaCmd::from_args([].into_iter()), Err(ArgsErr::NoArgs));
+ assert_eq!(MetaCmd::from_args(iter::empty()), Err(ArgsErr::NoArgs));
// We always ignore the first argument.
assert_eq!(
- MetaCmd::from_args([OsString::new()].into_iter()),
+ MetaCmd::from_args(iter::once(OsString::new())),
Ok(MetaCmd::Cargo(
Cmd::All(false, false, Ignored::None),
Opts {
diff --git a/src/cargo.rs b/src/cargo.rs
@@ -347,8 +347,8 @@ const ALWAYS: &str = "always";
const NEVER: &str = "never";
/// `"--no-default-features"`.
const DASH_DASH_NO_DEFAULT_FEATURES: &str = "--no-default-features";
-/// `"--features"`.
-const DASH_DASH_FEATURES: &str = "--features";
+/// `"-F"`.
+const DASH_F: &str = "-F";
/// `"--"`.
const DASH_DASH: &str = "--";
/// `"default"`.
@@ -507,7 +507,7 @@ impl Clippy {
.arg(if options.color { ALWAYS } else { NEVER })
.arg(DASH_DASH_NO_DEFAULT_FEATURES);
if !features.is_empty() {
- _ = c.arg(DASH_DASH_FEATURES).arg(features);
+ _ = c.arg(DASH_F).arg(features);
}
if deny_warnings {
_ = c.arg(DASH_DASH).arg("-Dwarnings");
@@ -565,7 +565,7 @@ impl Tests {
.arg(if options.color { ALWAYS } else { NEVER })
.arg(DASH_DASH_NO_DEFAULT_FEATURES);
if !features.is_empty() {
- _ = c.arg(DASH_DASH_FEATURES).arg(features);
+ _ = c.arg(DASH_F).arg(features);
}
let mut doc_only = false;
match kind {
diff --git a/src/manifest.rs b/src/manifest.rs
@@ -1,4 +1,3 @@
-extern crate alloc;
use super::cargo::{Toolchain, ToolchainErr};
use alloc::borrow::Cow;
use core::cmp::Ordering;
@@ -1086,25 +1085,23 @@ impl Features {
// so we don't need to continue.
Err(())
} else if feat.1.iter().any(|feat_dep| {
- let dep_name_utf8 = dep_name.as_bytes();
- let feat_dep_utf8 = feat_dep.as_bytes();
- dep_name_utf8 == feat_dep_utf8
- || feat_dep_utf8
- .split_at_checked(DEP.len())
- .is_some_and(|(pref, rem)| {
- pref == DEP
- && dep_name_utf8 == rem
- })
+ feat_dep
+ .as_bytes()
+ .split_at_checked(DEP.len())
+ .is_some_and(|(pref, rem)| {
+ pref == DEP
+ && dep_name.as_bytes()
+ == rem
+ })
}) {
- // The feature dependencies contain
- // `"dep:<dep_name>"` or `<dep_name>`. Either way,
- // we don't need to add an implied feature.
+ // The feature dependencies contain `"dep:<dep_name>"`,
+ // so we don't need to add an implied feature.
Err(())
} else {
// The feature name is not `<dep_name>` and all of
- // the feature dependencies are not named
- // `<dep_name` nor `"dep:<dep_name>"`; thus we need
- // to continue our search.
+ // feature dependencies of all features are not named
+ // `"dep:<dep_name>"`; thus we need to continue our
+ // search.
Ok(())
}
})
@@ -1331,21 +1328,19 @@ impl Manifest {
.add_implied_features(cargo, allow_implied_features)
.map_err(ManifestErr::ImpliedFeatures)
.map(|()| {
- if allow_implied_features {
- features.0.iter_mut().fold(
- (),
- |(), &mut (_, ref mut feat)| {
- feat.retain(|f| {
- // We retain only features. Since we didn't save any
- // dependencies that contain `'/'`, it's slightly faster to just
- // check that a feature dependency is not a dependency.
- !is_feature_dependency_a_dependency(
- f.as_bytes(),
- )
- });
- },
- );
- }
+ features.0.iter_mut().fold(
+ (),
+ |(), &mut (_, ref mut feat)| {
+ feat.retain(|f| {
+ // We retain only features. Since we didn't save any
+ // dependencies that contain `'/'`, it's slightly faster to just
+ // check that a feature dependency is not a dependency.
+ !is_feature_dependency_a_dependency(
+ f.as_bytes(),
+ )
+ });
+ },
+ );
Self { msrv, features }
})
})
@@ -1946,6 +1941,49 @@ mod tests {
features: Features(vec![("bar".to_owned(), Vec::new()), ("default".to_owned(), vec!["bar".to_owned()]), ("a".to_owned(), Vec::new()), ("\0".to_owned(), Vec::new()), ("fizz".to_owned(), Vec::new()), ("c".to_owned(), Vec::new())]),
})
);
+ // [package]
+ //
+ // [dependencies]
+ // foo = { "optional" = true }
+ // fizz = { "optional" = true }
+ //
+ // [features]
+ // fizz = ["dep:fizz"]
+ // bar = ["dep:foo"]
+ assert_eq!(
+ Manifest::from_toml(
+ "[package]\n[dependencies]\nfoo={\"optional\"=true}\nfizz={\"optional\"=true}\n[features]\nfizz=[\"dep:fizz\"]\nbar=[\"dep:foo\"]".to_owned(),
+ false,
+ ),
+ Ok(Manifest {
+ msrv: None,
+ features: Features(vec![
+ ("bar".to_owned(), Vec::new()),
+ ("fizz".to_owned(), Vec::new())
+ ]),
+ })
+ );
+ // [package]
+ //
+ // [dependencies]
+ // bar = { "optional" = true }
+ //
+ // [features]
+ // foo = ["bar"]
+ assert_eq!(
+ Manifest::from_toml(
+ "[package]\n[dependencies]\nbar={\"optional\"=true}\n[features]\nfoo=[\"bar\"]"
+ .to_owned(),
+ true,
+ ),
+ Ok(Manifest {
+ msrv: None,
+ features: Features(vec![
+ ("foo".to_owned(), vec!["bar".to_owned()]),
+ ("bar".to_owned(), Vec::new()),
+ ]),
+ })
+ );
}
#[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
#[expect(