Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3
4use super::{assignment_line, is_cargo_manifest};
5use crate::{Example, TomlCtx, Violation, violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example { label: "capability-named features", code: "[features]\ndefault = []\nstd = []\nserde = [\"dep:serde\"]\n", pass: true },
10    Example { label: "no features table", code: "[package]\nname = \"foo\"\n", pass: true },
11    Example { label: "use- prefix", code: "[features]\nuse-serde = [\"dep:serde\"]\n", pass: false },
12    Example { label: "with_ prefix", code: "[features]\nwith_tokio = [\"dep:tokio\"]\n", pass: false },
13    Example { label: "-support suffix", code: "[features]\nserde-support = [\"dep:serde\"]\n", pass: false },
14    Example { label: "_support suffix", code: "[features]\nserde_support = [\"dep:serde\"]\n", pass: false },
15];
16
17crate::toml_rule!(
18    toml_cargo_feature_names,
19    "Flag Cargo feature names with use-/with- prefixes or -support suffixes.",
20    "Feature names should describe the capability itself; placeholder affixes add noise without meaning (C-FEATURE).",
21    Low,
22    params {
23        allowed: [String] = [],
24    },
25);
26
27fn check_toml_cargo_feature_names(ctx: &TomlCtx<'_>) -> Vec<Violation> {
28    if !is_cargo_manifest(ctx.file.rel) || !ctx.parse.errors.is_empty() {
29        return Vec::new();
30    }
31
32    let Ok(document) = toml::from_str::<toml::Table>(ctx.file.contents) else {
33        return Vec::new();
34    };
35    let Some(features) = document.get("features").and_then(toml::Value::as_table) else {
36        return Vec::new();
37    };
38    let allowed = ctx
39        .file
40        .config
41        .get_str_array("toml_cargo_feature_names", &PARAMS[0]);
42
43    features
44        .keys()
45        .filter(|name| has_placeholder_affix(name) && !allowed.contains(name))
46        .map(|name| {
47            violation(
48                ctx.file.rel,
49                assignment_line(ctx.file.lines, name),
50                format!("feature `{name}` uses a placeholder affix; name the capability itself (e.g. `serde` instead of `use-serde` or `serde-support`)"),
51            )
52        })
53        .collect()
54}
55
56fn has_placeholder_affix(name: &str) -> bool {
57    const PREFIXES: &[&str] = &["use-", "use_", "with-", "with_"];
58    const SUFFIXES: &[&str] = &["-support", "_support"];
59
60    PREFIXES.iter().any(|prefix| name.starts_with(prefix))
61        || SUFFIXES.iter().any(|suffix| name.ends_with(suffix))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn run(source: &str) -> Vec<Violation> {
69        crate::test_support::check_source_toml_at(
70            "crates/foo/Cargo.toml",
71            source,
72            check_toml_cargo_feature_names,
73        )
74    }
75
76    crate::example_tests!(EXAMPLES, check_toml_cargo_feature_names);
77
78    #[gtest]
79    fn ignores_non_cargo_toml() -> Result<()> {
80        let violations = crate::test_support::check_source_toml_at(
81            "crates/foo/other.toml",
82            "[features]\nuse-serde = []\n",
83            check_toml_cargo_feature_names,
84        );
85
86        verify_true!(violations.is_empty())?;
87
88        Ok(())
89    }
90}