wowlab_tidy/languages/toml/rules/cargo/
workspace_dep_features.rs1#[cfg(test)]
2use googletest::prelude::*;
3
4use super::{CARGO_WORKSPACE_REL, cargo_document, key_line, nested_table};
5use crate::{Example, TomlCtx, Violation, violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example { label: "allowlisted features", code: "[workspace.dependencies]\nserde = { version = \"1\", features = [\"derive\", \"std\"] }\n", pass: true },
10 Example { label: "no features", code: "[workspace.dependencies]\nserde = \"1\"\nthiserror = { version = \"2\", default-features = false }\n", pass: true },
11 Example { label: "feature outside allowlist", code: "[workspace.dependencies]\ntsify = { version = \"0.5\", features = [\"js\"] }\n", pass: false },
12];
13
14crate::toml_rule!(
15 toml_cargo_workspace_dep_features,
16 "Flag [workspace.dependencies] entries that enable features outside the allowlist.",
17 "Features are additive and belong to the consuming crate; the workspace table should only pin versions so members do not inherit unwanted features (M-CARGO-WORKSPACE).",
18 Low,
19 params {
20 allowed_features: [String] = ["derive", "std"]
21 },
22);
23
24fn check_toml_cargo_workspace_dep_features(ctx: &TomlCtx<'_>) -> Vec<Violation> {
25 if ctx.file.rel != CARGO_WORKSPACE_REL {
26 return Vec::new();
27 }
28
29 let Some(document) = cargo_document(ctx) else {
30 return Vec::new();
31 };
32 let Some(dependencies) = nested_table(&document, &["workspace", "dependencies"]) else {
33 return Vec::new();
34 };
35 let allowed = ctx
36 .file
37 .config
38 .get_str_array("toml_cargo_workspace_dep_features", &PARAMS[0]);
39
40 let mut violations = Vec::new();
41
42 for (name, value) in dependencies {
43 let Some(features) = value
44 .as_table()
45 .and_then(|spec| spec.get("features"))
46 .and_then(toml::Value::as_array)
47 else {
48 continue;
49 };
50 let disallowed: Vec<&str> = features
51 .iter()
52 .filter_map(toml::Value::as_str)
53 .filter(|feature| !allowed.iter().any(|allowed| allowed == feature))
54 .collect();
55
56 if disallowed.is_empty() {
57 continue;
58 }
59
60 violations.push(violation(
61 ctx.file.rel,
62 key_line(ctx.file.lines, "workspace.dependencies", name),
63 format!(
64 "[workspace.dependencies] `{name}` enables features {disallowed:?}; \
65 consumers enable features, the workspace table only pins versions"
66 ),
67 ));
68 }
69
70 violations
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[gtest]
78 fn examples() -> Result<()> {
79 for example in EXAMPLES {
80 let violations = crate::test_support::check_source_toml_at(
81 "crates/Cargo.toml",
82 example.code,
83 check_toml_cargo_workspace_dep_features,
84 );
85
86 verify_eq!(violations.is_empty(), example.pass)?;
87 }
88
89 Ok(())
90 }
91
92 #[gtest]
93 fn member_manifests_are_not_checked() -> Result<()> {
94 let violations = crate::test_support::check_source_toml_at(
95 "crates/foo/Cargo.toml",
96 "[workspace.dependencies]\ntsify = { version = \"0.5\", features = [\"js\"] }\n",
97 check_toml_cargo_workspace_dep_features,
98 );
99
100 verify_true!(violations.is_empty())?;
101
102 Ok(())
103 }
104}