Skip to main content

wowlab_tidy/languages/toml/rules/cargo/
workspace_dep_features.rs

1#[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}