From 4230fd8ff495bdac20b0f6602503dfb365b1b2c1 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi Date: Wed, 13 Aug 2025 15:36:50 +0200 Subject: [PATCH 1/6] Enable optional deps as non-optional when their feature does not exist Signed-off-by: Oliver Tale-Yazdi --- .vscode/settings.json | 22 +++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- src/cmd/lint.rs | 42 ++++++++++++++++++++++++++-- src/cmd/mod.rs | 7 ++++- src/cmd/run.rs | 2 +- src/config/mod.rs | 9 +++++- src/config/workflow.rs | 6 ++-- tests/ui/config/v1/basic.yaml | 10 +++---- tests/ui/config/v1/finds_all.yaml | 16 +++++------ tests/ui/config/v1/version_bin.yaml | 26 ++++++++++++----- tests/ui/config/v1/version_file.yaml | 7 ++--- tests/ui/fmt/help.yaml | 4 +-- tests/ui/root-args/help.yaml | 6 ++-- tests/ui/root-args/version.yaml | 4 +-- 15 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..797eb30 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#ffeb33", + "activityBar.background": "#ffeb33", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#009f90", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#15202b99", + "sash.hoverBorder": "#ffeb33", + "statusBar.background": "#ffe600", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#ccb800", + "statusBarItem.remoteBackground": "#ffe600", + "statusBarItem.remoteForeground": "#15202b", + "titleBar.activeBackground": "#ffe600", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#ffe60099", + "titleBar.inactiveForeground": "#15202b99" + }, + "peacock.color": "#ffe600" +} diff --git a/Cargo.lock b/Cargo.lock index d1ba9a2..28cbf05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,7 +1787,7 @@ dependencies = [ [[package]] name = "zepter" -version = "1.82.0" +version = "1.82.1" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 9fd15b6..b93044f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zepter" -version = "1.82.0" +version = "1.82.1" edition = "2021" authors = [ "Oliver Tale-Yazdi" ] description = "Analyze, Fix and Format features in your Rust workspace." diff --git a/src/cmd/lint.rs b/src/cmd/lint.rs index 8bb28d3..5fdada4 100644 --- a/src/cmd/lint.rs +++ b/src/cmd/lint.rs @@ -147,7 +147,7 @@ pub struct PropagateFeatureCmd { #[clap(long, short, num_args(0..))] packages: Vec, - /// The auto-fixer will enables the feature of the dependencies as non-optional. + /// Enables the feature of the dependencies as non-optional. /// /// This can be used in case that a dependency should not be enabled like `dep?/feature` but /// like `dep/feature` instead. In this case you would pass `--feature-enables-dep @@ -471,6 +471,8 @@ impl PropagateFeatureCmd { let dep_kinds = self.parse_dep_kinds().expect("Parse dependency kinds"); // (Crate that missing the feature) -> (Dependency that has it) let mut feature_missing = BTreeMap::>::new(); + // (Crate that is not enabling a optional dependency) -> (Dependency that it is not enabled) + let mut non_optional_missing = BTreeMap::>::new(); for pkg in to_check.iter() { // TODO that it does not enable other features. @@ -492,15 +494,28 @@ impl PropagateFeatureCmd { continue }; + // If optional and require by `feature-enables-dep` but does not have the feature, + // then we need to enable it as non-optional. + if dep.optional { + if let Some((feature, _)) = self.feature_enables_dep.iter().flatten().find(|(f, name)| *f == feature && *name == dep.name()) { + if !dep.pkg.features.contains_key(feature) && !pkg.features.get(feature).is_some_and(|f| f.contains(&dep.name())) { + non_optional_missing.entry(pkg.id.to_string()).or_default().insert(dep.clone()); + } + } + // Continue here should not make a difference. TODO check. + } + if !dep.pkg.features.contains_key(&feature) { continue } + if !pkg.features.contains_key(&feature) { if self.left_side_feature_missing != MuteSetting::Ignore { feature_missing.entry(pkg.id.to_string()).or_default().insert(dep); } continue } + // TODO check that optional deps are only enabled as optional unless // overwritten with `--feature-enables-dep`. @@ -543,7 +558,7 @@ impl PropagateFeatureCmd { } } let faulty_crates: BTreeSet = - propagate_missing.keys().chain(feature_missing.keys()).cloned().collect(); + propagate_missing.keys().chain(feature_missing.keys()).chain(non_optional_missing.keys()).cloned().collect(); let mut faulty_crates = faulty_crates.into_iter().map(|id| (lookup(&id), id)).collect::>(); faulty_crates.sort_by(|(a, _), (b, _)| a.name.cmp(&b.name)); @@ -632,6 +647,29 @@ impl PropagateFeatureCmd { } errors += deps.len(); } + + if let Some(deps) = non_optional_missing.get(&krate.id.to_string()) { + let mut named = deps.iter().map(RenamedPackage::display_name).collect::>(); + named.sort(); + println!(" must enable dependency as non-optional:\n {}", named.join("\n ")); + + if self.fixer_args.enable && + self.fix_package.as_ref().is_none_or(|p| p == &krate.name.to_string()) + { + for dep in deps.iter() { + let dep_name = dep.name(); + let Some(fixer) = fixer.as_mut() else { continue }; + + fixer.add_to_feature(&feature, format!("{dep_name}").as_str()).unwrap(); + + log::info!("Inserted '{dep_name}' into '{}'", krate.name); + fixes += 1; + } + } + + errors += deps.len(); + } + if let Some(fixer) = fixer.as_mut() { if fixes > 0 { fixer.save().unwrap(); diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index c5810ff..c03d957 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -48,7 +48,7 @@ pub struct GlobalArgs { /// Try to exit with code zero if the intended check failed. /// - /// Will still return 1 in case of an actual error (eg. failed to find some file) or a panic + /// Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic /// (aka software bug). #[clap(long, global = true, verbatim_doc_comment)] exit_code_zero: bool, @@ -145,6 +145,11 @@ impl GlobalArgs { } } + /// Error code when the config file is invalid or incompatible. + pub fn error_code_cfg_parsing() -> i32 { + 101 + } + pub fn red(&self, s: &str) -> String { if !self.color { s.to_string() diff --git a/src/cmd/run.rs b/src/cmd/run.rs index 84f8eb1..2736c0c 100644 --- a/src/cmd/run.rs +++ b/src/cmd/run.rs @@ -24,7 +24,7 @@ pub struct RunArgs { impl RunCmd { pub fn run(&self, g: &GlobalArgs) { - let config = self.args.config.load().expect("Invalid config file"); + let config = self.args.config.load_or_panic(); let name = self.args.workflow.as_deref().unwrap_or(WORKFLOW_DEFAULT_NAME); let Some(workflow) = config.workflow(name) else { diff --git a/src/config/mod.rs b/src/config/mod.rs index 40f2444..a5abc17 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,7 +4,7 @@ pub mod semver; pub mod workflow; -use crate::{config::workflow::WorkflowFile, log, ErrToStr}; +use crate::{cmd::GlobalArgs, config::workflow::WorkflowFile, log, ErrToStr}; use std::{ fs::canonicalize, @@ -72,6 +72,13 @@ pub fn search_config>(workspace: P) -> Result WorkflowFile { + self.load().unwrap_or_else(|e| { + eprintln!("{e}"); + std::process::exit(GlobalArgs::error_code_cfg_parsing()); + }) + } + pub fn load(&self) -> Result { let path = self.locate_config()?; log::debug!("Using config file: {path:?}"); diff --git a/src/config/workflow.rs b/src/config/workflow.rs index 72a10e6..2f2a8ac 100644 --- a/src/config/workflow.rs +++ b/src/config/workflow.rs @@ -166,13 +166,13 @@ impl WorkflowFile { pub fn check_cfg_compatibility(&self) -> Result<(), String> { let current_version = Semver::try_from(clap::crate_version!()).expect("Crate version is valid semver"); - let file_version = self.version.binary; + let required_version = self.version.binary; - if current_version.is_newer_or_equal(&file_version) { + if current_version.is_newer_or_equal(&required_version) { Ok(()) } else { Err(format!( - "Config file version is too new. The file requires at least version {file_version}, but the current version is {current_version}. Please update Zepter or ignore this check with `--check-cfg-compatibility=off`." + "Your version of Zepter is too old for this project.\n\n Required: {required_version}\n Installed: {current_version}\n\nPlease update Zepter with:\n\n cargo install zepter --locked\n\nOr add `--check-cfg-compatibility=off` to the config file." )) } } diff --git a/tests/ui/config/v1/basic.yaml b/tests/ui/config/v1/basic.yaml index 211ed1e..2c4be3b 100644 --- a/tests/ui/config/v1/basic.yaml +++ b/tests/ui/config/v1/basic.yaml @@ -3,25 +3,25 @@ crates: cases: - cmd: run default stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version - cmd: run stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version - cmd: '' stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version - cmd: run my_version stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'my_version' [INFO] 1/1 --version @@ -36,7 +36,7 @@ cases: [INFO] 1/1 debug --no-benchmark - cmd: run both stdout: | - zepter 1.82.0 + zepter 1.82.1 Num workspace members: 1 Num dependencies: 1 DAG nodes: 0, links: 0 diff --git a/tests/ui/config/v1/finds_all.yaml b/tests/ui/config/v1/finds_all.yaml index 0cbdcdc..bba2d20 100644 --- a/tests/ui/config/v1/finds_all.yaml +++ b/tests/ui/config/v1/finds_all.yaml @@ -3,7 +3,7 @@ crates: cases: - cmd: '' stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -19,7 +19,7 @@ cases: - [ '--version' ] - cmd: '' stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -35,7 +35,7 @@ cases: - [ '--version' ] - cmd: '' stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -51,7 +51,7 @@ cases: - [ '--version' ] - cmd: run default stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -67,7 +67,7 @@ cases: - [ '--version' ] - cmd: run default stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -83,7 +83,7 @@ cases: - [ '--version' ] - cmd: run default stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -99,7 +99,7 @@ cases: - [ '--version' ] - cmd: run default --config .cargo/polkadot.yaml stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version @@ -115,7 +115,7 @@ cases: - [ '--version' ] - cmd: run default -c .cargo/polkadot.yaml stdout: | - zepter 1.82.0 + zepter 1.82.1 stderr: | [INFO] Running workflow 'default' [INFO] 1/1 --version diff --git a/tests/ui/config/v1/version_bin.yaml b/tests/ui/config/v1/version_bin.yaml index 2f06479..965085a 100644 --- a/tests/ui/config/v1/version_bin.yaml +++ b/tests/ui/config/v1/version_bin.yaml @@ -2,21 +2,33 @@ crates: - name: A cases: - cmd: run default - stderr: |2 + stderr: | + Your version of Zepter is too old for this project. + + Required: 2.0.0 + Installed: 1.82.1 + + Please update Zepter with: + + cargo install zepter --locked - thread 'main' panicked at src/cmd/run.rs:27:46: - Invalid config file: "Config file version is too new. The file requires at least version 2.0.0, but the current version is 1.82.0. Please update Zepter or ignore this check with `--check-cfg-compatibility=off`." - note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + Or add `--check-cfg-compatibility=off` to the config file. code: 101 - cmd: run default --check-cfg-compatibility=off stdout: | Error: Command '' failed with exit code 101 stderr: | [INFO] Running workflow 'default' + Your version of Zepter is too old for this project. + + Required: 2.0.0 + Installed: 1.82.1 + + Please update Zepter with: + + cargo install zepter --locked - thread 'main' panicked at src/cmd/run.rs:27:46: - Invalid config file: "Config file version is too new. The file requires at least version 2.0.0, but the current version is 1.82.0. Please update Zepter or ignore this check with `--check-cfg-compatibility=off`." - note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + Or add `--check-cfg-compatibility=off` to the config file. code: 1 configs: - to_path: .zepter.yaml diff --git a/tests/ui/config/v1/version_file.yaml b/tests/ui/config/v1/version_file.yaml index d970c83..085518b 100644 --- a/tests/ui/config/v1/version_file.yaml +++ b/tests/ui/config/v1/version_file.yaml @@ -2,11 +2,8 @@ crates: - name: A cases: - cmd: run default - stderr: |2 - - thread 'main' panicked at src/cmd/run.rs:27:46: - Invalid config file: "Can only parse workflow files with version '1'" - note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + stderr: | + Can only parse workflow files with version '1' code: 101 configs: - to_path: .zepter.yaml diff --git a/tests/ui/fmt/help.yaml b/tests/ui/fmt/help.yaml index 058eb8a..62a71c0 100644 --- a/tests/ui/fmt/help.yaml +++ b/tests/ui/fmt/help.yaml @@ -1,6 +1,6 @@ crates: [] cases: - cmd: format --help - stdout: "Format the features in your manifest files\n\nUsage: zepter format [OPTIONS] \n\nCommands:\n features Format the content of each feature in the crate manifest\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return 1 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n -h, --help\n Print help (see a summary with '-h')\n" + stdout: "Format the features in your manifest files\n\nUsage: zepter format [OPTIONS] \n\nCommands:\n features Format the content of each feature in the crate manifest\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n -h, --help\n Print help (see a summary with '-h')\n" - cmd: format features --help - stdout: "Format the content of each feature in the crate manifest\n\nUsage: zepter format features [OPTIONS]\n\nOptions:\n --manifest-path \n Cargo manifest path or directory.\n \n For directories it appends a `Cargo.toml`.\n\n --workspace\n Whether to only consider workspace crates\n\n --offline\n Whether to use offline mode\n\n --locked\n Whether to use all the locked dependencies from the `Cargo.lock`.\n \n Otherwise it may update some dependencies. For CI usage its a good idea to use it.\n\n --all-features\n \n\n --no-workspace\n Include dependencies in the formatting check.\n \n They will not be modified, unless their path is included in `--modify-paths`.\n\n --modify-paths \n Paths that are allowed to be modified by the formatter\n\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n -c, --check\n DEPRECATED AND IGNORED\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n -f, --fix\n Fix the formatting errors automatically\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return 1 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --line-width \n The maximal length of a line for a feature\n \n [default: 80]\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n --mode-per-feature \n Set the formatting mode for a specific feature.\n \n Can be specified multiple times. Example:\n `--mode-per-feature default:sort,default:canonicalize`\n\n --ignore-feature \n Ignore a specific feature across all crates.\n \n This is equivalent to `--mode-per-feature FEATURE:none`.\n\n --print-paths\n Also print the paths of the offending Cargo.toml files\n\n -h, --help\n Print help (see a summary with '-h')\n" + stdout: "Format the content of each feature in the crate manifest\n\nUsage: zepter format features [OPTIONS]\n\nOptions:\n --manifest-path \n Cargo manifest path or directory.\n \n For directories it appends a `Cargo.toml`.\n\n --workspace\n Whether to only consider workspace crates\n\n --offline\n Whether to use offline mode\n\n --locked\n Whether to use all the locked dependencies from the `Cargo.lock`.\n \n Otherwise it may update some dependencies. For CI usage its a good idea to use it.\n\n --all-features\n \n\n --no-workspace\n Include dependencies in the formatting check.\n \n They will not be modified, unless their path is included in `--modify-paths`.\n\n --modify-paths \n Paths that are allowed to be modified by the formatter\n\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n -c, --check\n DEPRECATED AND IGNORED\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n -f, --fix\n Fix the formatting errors automatically\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --line-width \n The maximal length of a line for a feature\n \n [default: 80]\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n --mode-per-feature \n Set the formatting mode for a specific feature.\n \n Can be specified multiple times. Example:\n `--mode-per-feature default:sort,default:canonicalize`\n\n --ignore-feature \n Ignore a specific feature across all crates.\n \n This is equivalent to `--mode-per-feature FEATURE:none`.\n\n --print-paths\n Also print the paths of the offending Cargo.toml files\n\n -h, --help\n Print help (see a summary with '-h')\n" diff --git a/tests/ui/root-args/help.yaml b/tests/ui/root-args/help.yaml index 207005c..d9085b6 100644 --- a/tests/ui/root-args/help.yaml +++ b/tests/ui/root-args/help.yaml @@ -1,8 +1,8 @@ crates: [] cases: - cmd: --help - stdout: "Analyze, Fix and Format features in your Rust workspace.\n\nUsage: zepter [OPTIONS] [COMMAND]\n\nCommands:\n trace Trace the dependency path from one crate to another\n lint Lint your feature usage by analyzing crate metadata\n format Format the features in your manifest files\n run \n debug Arguments for how to load cargo metadata from a workspace\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return 1 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n -h, --help\n Print help (see a summary with '-h')\n\n -V, --version\n Print version\n" + stdout: "Analyze, Fix and Format features in your Rust workspace.\n\nUsage: zepter [OPTIONS] [COMMAND]\n\nCommands:\n trace Trace the dependency path from one crate to another\n lint Lint your feature usage by analyzing crate metadata\n format Format the features in your manifest files\n run \n debug Arguments for how to load cargo metadata from a workspace\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n -h, --help\n Print help (see a summary with '-h')\n\n -V, --version\n Print version\n" - cmd: lint --help - stdout: "Lint your feature usage by analyzing crate metadata\n\nUsage: zepter lint [OPTIONS] \n\nCommands:\n propagate-feature Check whether features are properly propagated\n never-enables A specific feature never enables a specific other feature\n never-implies A specific feature never implies a specific other feature\n only-enables A specific feature is only implied by a specific set of other features\n why-enabled Arguments for how to load cargo metadata from a workspace\n no-std Check the crates for sane no-std feature configuration\n duplicate-deps Check for duplicated dependencies in `[dependencies]` and `[dev-dependencies]`\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return 1 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n -h, --help\n Print help (see a summary with '-h')\n" + stdout: "Lint your feature usage by analyzing crate metadata\n\nUsage: zepter lint [OPTIONS] \n\nCommands:\n propagate-feature Check whether features are properly propagated\n never-enables A specific feature never enables a specific other feature\n never-implies A specific feature never implies a specific other feature\n only-enables A specific feature is only implied by a specific set of other features\n why-enabled Arguments for how to load cargo metadata from a workspace\n no-std Check the crates for sane no-std feature configuration\n duplicate-deps Check for duplicated dependencies in `[dependencies]` and `[dev-dependencies]`\n help Print this message or the help of the given subcommand(s)\n\nOptions:\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n -h, --help\n Print help (see a summary with '-h')\n" - cmd: lint propagate-feature --help - stdout: "Check whether features are properly propagated\n\nUsage: zepter lint propagate-feature [OPTIONS] --features \n\nOptions:\n --manifest-path \n Cargo manifest path or directory.\n \n For directories it appends a `Cargo.toml`.\n\n --workspace\n Whether to only consider workspace crates\n\n --offline\n Whether to use offline mode\n\n --locked\n Whether to use all the locked dependencies from the `Cargo.lock`.\n \n Otherwise it may update some dependencies. For CI usage its a good idea to use it.\n\n --all-features\n \n\n --features \n Comma separated list of features to check.\n \n Listing the same feature multiple times has the same effect as listing it once.\n\n -p, --packages [...]\n The packages to check. If empty, all packages are checked\n\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --feature-enables-dep \n The auto-fixer will enables the feature of the dependencies as non-optional.\n \n This can be used in case that a dependency should not be enabled like `dep?/feature` but\n like `dep/feature` instead. In this case you would pass `--feature-enables-dep\n feature:dep`. The option can be passed multiple times, or multiple key-value pairs can be\n passed at once by separating them with a comma like: `--feature-enables-dep\n feature:dep,feature2:dep2`. (TODO: Duplicate entries are undefined).\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --left-side-feature-missing \n Overwrite the behaviour when the left side dependency is missing the feature.\n \n This can be used to ignore missing features, treat them as warning or error. A \"missing\n feature\" here means that if `A` has a dependency `B` which has a feature `F`, and the\n propagation is checked then normally it would error if `A` is not forwarding `F` to `B`.\n Now this option modifies the behaviour if `A` does not have the feature in the first place.\n The default behaviour is to require `A` to also have `F`.\n \n [default: fix]\n\n Possible values:\n - ignore: Ignore this behaviour\n - report: Only report but do not fix\n - fix: Fix if `--fix` is passed\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return 1 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --ignore-missing-propagate \n Ignore single missing links in the feature propagation chain.\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n --left-side-outside-workspace \n How to handle the case that the LHS is outside the workspace.\n \n [default: fix]\n\n Possible values:\n - ignore: Ignore this behaviour\n - report: Only report but do not fix\n - fix: Fix if `--fix` is passed\n\n --dep-kinds \n How to handle dev-dependencies.\n \n [default: normal:check,dev:check,build:check]\n\n --show-version\n Show crate versions in the output\n\n --show-path\n Show crate manifest paths in the output\n\n --fix\n Try to automatically fix the problems\n\n --modify-paths \n \n\n --fix-dependency \n Fix only issues with this package as dependency\n\n --fix-package \n Fix only issues with this package as feature source\n\n -h, --help\n Print help (see a summary with '-h')\n" + stdout: "Check whether features are properly propagated\n\nUsage: zepter lint propagate-feature [OPTIONS] --features \n\nOptions:\n --manifest-path \n Cargo manifest path or directory.\n \n For directories it appends a `Cargo.toml`.\n\n --workspace\n Whether to only consider workspace crates\n\n --offline\n Whether to use offline mode\n\n --locked\n Whether to use all the locked dependencies from the `Cargo.lock`.\n \n Otherwise it may update some dependencies. For CI usage its a good idea to use it.\n\n --all-features\n \n\n --features \n Comma separated list of features to check.\n \n Listing the same feature multiple times has the same effect as listing it once.\n\n -p, --packages [...]\n The packages to check. If empty, all packages are checked\n\n -q, --quiet\n Only print errors. Supersedes `--log`\n\n --feature-enables-dep \n Enables the feature of the dependencies as non-optional.\n \n This can be used in case that a dependency should not be enabled like `dep?/feature` but\n like `dep/feature` instead. In this case you would pass `--feature-enables-dep\n feature:dep`. The option can be passed multiple times, or multiple key-value pairs can be\n passed at once by separating them with a comma like: `--feature-enables-dep\n feature:dep,feature2:dep2`. (TODO: Duplicate entries are undefined).\n\n --log \n Log level to use\n \n [default: info]\n\n --color\n Use ANSI terminal colors\n\n --left-side-feature-missing \n Overwrite the behaviour when the left side dependency is missing the feature.\n \n This can be used to ignore missing features, treat them as warning or error. A \"missing\n feature\" here means that if `A` has a dependency `B` which has a feature `F`, and the\n propagation is checked then normally it would error if `A` is not forwarding `F` to `B`.\n Now this option modifies the behaviour if `A` does not have the feature in the first place.\n The default behaviour is to require `A` to also have `F`.\n \n [default: fix]\n\n Possible values:\n - ignore: Ignore this behaviour\n - report: Only report but do not fix\n - fix: Fix if `--fix` is passed\n\n --exit-code-zero\n Try to exit with code zero if the intended check failed.\n \n Will still return != 0 in case of an actual error (eg. failed to find some file) or a panic\n (aka software bug).\n\n --ignore-missing-propagate \n Ignore single missing links in the feature propagation chain.\n\n --fix-hint \n Dont print any hints on how to fix the error.\n \n This is mostly used internally when dispatching, workflows since they come with their\n hints.\n \n [default: on]\n\n Possible values:\n - on: Prints some hint that is (hopefully) helpful\n - off: Prints no hint at all\n\n --left-side-outside-workspace \n How to handle the case that the LHS is outside the workspace.\n \n [default: fix]\n\n Possible values:\n - ignore: Ignore this behaviour\n - report: Only report but do not fix\n - fix: Fix if `--fix` is passed\n\n --dep-kinds \n How to handle dev-dependencies.\n \n [default: normal:check,dev:check,build:check]\n\n --show-version\n Show crate versions in the output\n\n --show-path\n Show crate manifest paths in the output\n\n --fix\n Try to automatically fix the problems\n\n --modify-paths \n \n\n --fix-dependency \n Fix only issues with this package as dependency\n\n --fix-package \n Fix only issues with this package as feature source\n\n -h, --help\n Print help (see a summary with '-h')\n" diff --git a/tests/ui/root-args/version.yaml b/tests/ui/root-args/version.yaml index 50872f7..6e0545f 100644 --- a/tests/ui/root-args/version.yaml +++ b/tests/ui/root-args/version.yaml @@ -2,7 +2,7 @@ crates: [] cases: - cmd: --version stdout: | - zepter 1.82.0 + zepter 1.82.1 - cmd: -V stdout: | - zepter 1.82.0 + zepter 1.82.1 From 23c3b84be8960a6d582fc240c38ac8dd5c0d9e6f Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi Date: Wed, 13 Aug 2025 16:33:32 +0200 Subject: [PATCH 2/6] Tests Signed-off-by: Oliver Tale-Yazdi --- .../feature_enables_dep.yaml | 25 +++++++++++++++++ .../feature_enables_dep_multiple.yaml | 28 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/ui/lint/propagate-feature/feature_enables_dep.yaml create mode 100644 tests/ui/lint/propagate-feature/feature_enables_dep_multiple.yaml diff --git a/tests/ui/lint/propagate-feature/feature_enables_dep.yaml b/tests/ui/lint/propagate-feature/feature_enables_dep.yaml new file mode 100644 index 0000000..6185033 --- /dev/null +++ b/tests/ui/lint/propagate-feature/feature_enables_dep.yaml @@ -0,0 +1,25 @@ +crates: +- name: A + deps: + - name: B + optional: true + features: + F0: null +- name: B +cases: +- cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B --fix + stdout: | + crate 'A' + feature 'F0' + must enable dependency as non-optional: + B + Found 1 issue and fixed 1 (all fixed). + diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 3332f79684..26dd2b015c 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -15,0 +16 @@ F0 = [\n+\t\"B\"\n" +- cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B + stdout: | + crate 'A' + feature 'F0' + must enable dependency as non-optional: + B + Found 1 issue (run with `--fix` to fix). + code: 1 diff --git a/tests/ui/lint/propagate-feature/feature_enables_dep_multiple.yaml b/tests/ui/lint/propagate-feature/feature_enables_dep_multiple.yaml new file mode 100644 index 0000000..0dc1143 --- /dev/null +++ b/tests/ui/lint/propagate-feature/feature_enables_dep_multiple.yaml @@ -0,0 +1,28 @@ +crates: +- name: A + deps: + - name: B + optional: true + - name: C + optional: true + features: + F0: null + F1: null +- name: B +- name: C + features: + F1: null +cases: +- cmd: lint propagate-feature --feature F0,F1 --feature-enables-dep F0:B,F1:C --fix + stdout: | + crate 'A' + feature 'F0' + must enable dependency as non-optional: + B + Found 1 issue and fixed 1 (all fixed). + crate 'A' + feature 'F1' + must propagate to: + C + Found 1 issue and fixed 1 (all fixed). + diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 957fb1d723..0d14f8f860 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -16,0 +17 @@ F0 = [\n+\t\"B\"\n@@ -18,0 +20 @@ F1 = [\n+\t\"C/F1\"\n" From 9c80cf4eda7b883b3073b72345b990b5772ea3b3 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi Date: Wed, 13 Aug 2025 16:43:16 +0200 Subject: [PATCH 3/6] tests Signed-off-by: Oliver Tale-Yazdi --- .../feature_enables_dep_no_match.yaml | 10 +++++++++ .../feature_enables_dep_non_optional.yaml | 9 ++++++++ ...e_enables_dep_same_feature_multi_deps.yaml | 21 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 tests/ui/lint/propagate-feature/feature_enables_dep_no_match.yaml create mode 100644 tests/ui/lint/propagate-feature/feature_enables_dep_non_optional.yaml create mode 100644 tests/ui/lint/propagate-feature/feature_enables_dep_same_feature_multi_deps.yaml diff --git a/tests/ui/lint/propagate-feature/feature_enables_dep_no_match.yaml b/tests/ui/lint/propagate-feature/feature_enables_dep_no_match.yaml new file mode 100644 index 0000000..858b7f1 --- /dev/null +++ b/tests/ui/lint/propagate-feature/feature_enables_dep_no_match.yaml @@ -0,0 +1,10 @@ +crates: +- name: A + deps: + - name: B + optional: true + features: + F0: null +- name: B +cases: +- cmd: lint propagate-feature --feature F0 --feature-enables-dep F1:B \ No newline at end of file diff --git a/tests/ui/lint/propagate-feature/feature_enables_dep_non_optional.yaml b/tests/ui/lint/propagate-feature/feature_enables_dep_non_optional.yaml new file mode 100644 index 0000000..13c74a1 --- /dev/null +++ b/tests/ui/lint/propagate-feature/feature_enables_dep_non_optional.yaml @@ -0,0 +1,9 @@ +crates: +- name: A + deps: + - name: B # Non-optional dependency + features: + F0: null +- name: B +cases: +- cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B \ No newline at end of file diff --git a/tests/ui/lint/propagate-feature/feature_enables_dep_same_feature_multi_deps.yaml b/tests/ui/lint/propagate-feature/feature_enables_dep_same_feature_multi_deps.yaml new file mode 100644 index 0000000..001b0bb --- /dev/null +++ b/tests/ui/lint/propagate-feature/feature_enables_dep_same_feature_multi_deps.yaml @@ -0,0 +1,21 @@ +crates: +- name: A + deps: + - name: B + optional: true + - name: C + optional: true + features: + F0: null +- name: B +- name: C +cases: +- cmd: lint propagate-feature --feature F0 --feature-enables-dep F0:B,F0:C --fix + stdout: | + crate 'A' + feature 'F0' + must enable dependency as non-optional: + B + C + Found 2 issues and fixed 2 (all fixed). + diff: "diff --git A/Cargo.toml A/Cargo.toml\nindex 6e53f70e68..6dda058f80 100644\n--- A/Cargo.toml\n+++ A/Cargo.toml\n@@ -16,0 +17,2 @@ F0 = [\n+\t\"B\",\n+\t\"C\"\n" From 70a474d26013395a44f13a4e392c554426b60cd5 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi Date: Wed, 13 Aug 2025 16:48:54 +0200 Subject: [PATCH 4/6] README Signed-off-by: Oliver Tale-Yazdi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2097a5..fe1f0d3 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ When these two experiments proove the usefulness and reliability of Zepter for C ## Testing Unit tests: `cargo test` -UI and downstream integration tests: `cargo test -- --ignored` +UI and downstream integration tests: `cargo test -- --ignored --nocapture` Environment overwrites exist for the UI tests to: - `OVERWRITE`: Update the UI diff locks. From ad5b5d7ee8465d7a4623fb0d4789971a18a23084 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi Date: Wed, 13 Aug 2025 16:59:56 +0200 Subject: [PATCH 5/6] fix clippy Signed-off-by: Oliver Tale-Yazdi --- src/cmd/lint.rs | 38 +++++++++++++++++++++++++++----------- src/config/mod.rs | 2 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/cmd/lint.rs b/src/cmd/lint.rs index 5fdada4..2a56865 100644 --- a/src/cmd/lint.rs +++ b/src/cmd/lint.rs @@ -497,9 +497,19 @@ impl PropagateFeatureCmd { // If optional and require by `feature-enables-dep` but does not have the feature, // then we need to enable it as non-optional. if dep.optional { - if let Some((feature, _)) = self.feature_enables_dep.iter().flatten().find(|(f, name)| *f == feature && *name == dep.name()) { - if !dep.pkg.features.contains_key(feature) && !pkg.features.get(feature).is_some_and(|f| f.contains(&dep.name())) { - non_optional_missing.entry(pkg.id.to_string()).or_default().insert(dep.clone()); + if let Some((feature, _)) = self + .feature_enables_dep + .iter() + .flatten() + .find(|(f, name)| *f == feature && *name == dep.name()) + { + if !dep.pkg.features.contains_key(feature) && + !pkg.features.get(feature).is_some_and(|f| f.contains(&dep.name())) + { + non_optional_missing + .entry(pkg.id.to_string()) + .or_default() + .insert(dep.clone()); } } // Continue here should not make a difference. TODO check. @@ -508,14 +518,13 @@ impl PropagateFeatureCmd { if !dep.pkg.features.contains_key(&feature) { continue } - + if !pkg.features.contains_key(&feature) { if self.left_side_feature_missing != MuteSetting::Ignore { feature_missing.entry(pkg.id.to_string()).or_default().insert(dep); } continue } - // TODO check that optional deps are only enabled as optional unless // overwritten with `--feature-enables-dep`. @@ -557,8 +566,12 @@ impl PropagateFeatureCmd { propagate_missing.entry(pkg.id.to_string()).or_default().insert(dep); } } - let faulty_crates: BTreeSet = - propagate_missing.keys().chain(feature_missing.keys()).chain(non_optional_missing.keys()).cloned().collect(); + let faulty_crates: BTreeSet = propagate_missing + .keys() + .chain(feature_missing.keys()) + .chain(non_optional_missing.keys()) + .cloned() + .collect(); let mut faulty_crates = faulty_crates.into_iter().map(|id| (lookup(&id), id)).collect::>(); faulty_crates.sort_by(|(a, _), (b, _)| a.name.cmp(&b.name)); @@ -651,8 +664,11 @@ impl PropagateFeatureCmd { if let Some(deps) = non_optional_missing.get(&krate.id.to_string()) { let mut named = deps.iter().map(RenamedPackage::display_name).collect::>(); named.sort(); - println!(" must enable dependency as non-optional:\n {}", named.join("\n ")); - + println!( + " must enable dependency as non-optional:\n {}", + named.join("\n ") + ); + if self.fixer_args.enable && self.fix_package.as_ref().is_none_or(|p| p == &krate.name.to_string()) { @@ -660,8 +676,8 @@ impl PropagateFeatureCmd { let dep_name = dep.name(); let Some(fixer) = fixer.as_mut() else { continue }; - fixer.add_to_feature(&feature, format!("{dep_name}").as_str()).unwrap(); - + fixer.add_to_feature(&feature, &dep_name).unwrap(); + log::info!("Inserted '{dep_name}' into '{}'", krate.name); fixes += 1; } diff --git a/src/config/mod.rs b/src/config/mod.rs index a5abc17..caec955 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -72,7 +72,7 @@ pub fn search_config>(workspace: P) -> Result WorkflowFile { + pub fn load_or_panic(&self) -> WorkflowFile { self.load().unwrap_or_else(|e| { eprintln!("{e}"); std::process::exit(GlobalArgs::error_code_cfg_parsing()); From 14d288689bc5c9d748f10ecb1de4ed7f3902cd37 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi Date: Wed, 13 Aug 2025 19:04:45 +0200 Subject: [PATCH 6/6] fix version Signed-off-by: Oliver Tale-Yazdi --- .github/workflows/downstream_nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/downstream_nightly.yml b/.github/workflows/downstream_nightly.yml index 45c6ef3..e24e995 100644 --- a/.github/workflows/downstream_nightly.yml +++ b/.github/workflows/downstream_nightly.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: repo: ["paritytech/polkadot-sdk", "open-web3-stack/open-runtime-module-library"] - version: [0.13.3, 1.4.0, "*"] + version: [1.78.0, 1.82.0, "*"] steps: - uses: actions/checkout@master