Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3
4use super::{
5    CARGO_WORKSPACE_REL, cargo_document, is_cargo_member, key_line, nested_table, section_line,
6};
7use crate::{Example, RuleParam, TomlCtx, Violation, violation};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11    Example {
12        label: "workspace with full lint set",
13        code: r#"[workspace.lints.rust]
14ambiguous_negative_literals = "warn"
15missing_debug_implementations = "warn"
16redundant_imports = "warn"
17redundant_lifetimes = "warn"
18trivial_numeric_casts = "warn"
19unsafe_op_in_unsafe_fn = "warn"
20unused_lifetimes = "warn"
21
22[workspace.lints.clippy]
23cargo = { level = "warn", priority = -1 }
24complexity = { level = "warn", priority = -1 }
25correctness = { level = "deny", priority = -1 }
26pedantic = { level = "warn", priority = -1 }
27perf = { level = "warn", priority = -1 }
28style = { level = "warn", priority = -1 }
29suspicious = { level = "warn", priority = -1 }
30allow_attributes_without_reason = "warn"
31as_pointer_underscore = "warn"
32assertions_on_result_states = "warn"
33clone_on_ref_ptr = "warn"
34deref_by_slicing = "warn"
35disallowed_script_idents = "warn"
36empty_drop = "warn"
37empty_enum_variants_with_brackets = "warn"
38empty_structs_with_brackets = "warn"
39fn_to_numeric_cast_any = "warn"
40if_then_some_else_none = "warn"
41implicit_clone = "warn"
42map_err_ignore = "warn"
43redundant_type_annotations = "warn"
44renamed_function_params = "warn"
45semicolon_outside_block = "warn"
46undocumented_unsafe_blocks = "warn"
47unnecessary_safety_comment = "warn"
48unnecessary_safety_doc = "warn"
49unneeded_field_pattern = "warn"
50unused_result_ok = "warn"
51"#,
52        pass: true,
53    },
54    Example { label: "workspace missing required lints", code: "[workspace]\nmembers = []\n", pass: false },
55    Example { label: "workspace lint downgraded to allow", code: "[workspace.lints.rust]\nunsafe_op_in_unsafe_fn = \"allow\"\n", pass: false },
56    Example { label: "member inherits workspace lints", code: "[package]\nname = \"foo\"\n\n[lints]\nworkspace = true\n", pass: true },
57    Example { label: "member without lint inheritance", code: "[package]\nname = \"foo\"\n", pass: false },
58];
59
60crate::toml_rule!(
61    toml_cargo_workspace_lints,
62    "Require the workspace to enable the standard rust/clippy lint set and members to inherit it via `[lints] workspace = true`.",
63    "Static verification only catches issues when every crate compiles under the same vetted workspace lint tables (M-STATIC-VERIFICATION).",
64    Medium,
65    params {
66        required_rust: [String] = [
67            "ambiguous_negative_literals",
68            "missing_debug_implementations",
69            "redundant_imports",
70            "redundant_lifetimes",
71            "trivial_numeric_casts",
72            "unsafe_op_in_unsafe_fn",
73            "unused_lifetimes",
74        ],
75        required_clippy: [String] = [
76            "cargo",
77            "complexity",
78            "correctness",
79            "pedantic",
80            "perf",
81            "style",
82            "suspicious",
83            "allow_attributes_without_reason",
84            "as_pointer_underscore",
85            "assertions_on_result_states",
86            "clone_on_ref_ptr",
87            "deref_by_slicing",
88            "disallowed_script_idents",
89            "empty_drop",
90            "empty_enum_variants_with_brackets",
91            "empty_structs_with_brackets",
92            "fn_to_numeric_cast_any",
93            "if_then_some_else_none",
94            "implicit_clone",
95            "map_err_ignore",
96            "redundant_type_annotations",
97            "renamed_function_params",
98            "semicolon_outside_block",
99            "undocumented_unsafe_blocks",
100            "unnecessary_safety_comment",
101            "unnecessary_safety_doc",
102            "unneeded_field_pattern",
103            "unused_result_ok",
104        ],
105    },
106);
107
108fn check_toml_cargo_workspace_lints(ctx: &TomlCtx<'_>) -> Vec<Violation> {
109    let Some(document) = cargo_document(ctx) else {
110        return Vec::new();
111    };
112
113    if ctx.file.rel == CARGO_WORKSPACE_REL {
114        let mut violations = Vec::new();
115
116        required_group_violations(ctx, &document, "rust", &PARAMS[0], &mut violations);
117        required_group_violations(ctx, &document, "clippy", &PARAMS[1], &mut violations);
118
119        return violations;
120    }
121
122    if is_cargo_member(ctx.file.rel) && document.contains_key("package") {
123        return member_violations(ctx, &document);
124    }
125
126    Vec::new()
127}
128
129fn required_group_violations(
130    ctx: &TomlCtx<'_>,
131    document: &toml::Table,
132    group: &str,
133    param: &RuleParam,
134    violations: &mut Vec<Violation>,
135) {
136    let section = format!("workspace.lints.{group}");
137    let table = nested_table(document, &["workspace", "lints", group]);
138
139    for lint in ctx
140        .file
141        .config
142        .get_str_array("toml_cargo_workspace_lints", param)
143    {
144        match table.and_then(|table| table.get(&lint)) {
145            Some(value) if lint_level(value) == Some("allow") => violations.push(violation(
146                ctx.file.rel,
147                key_line(ctx.file.lines, &section, &lint),
148                format!("[{section}] sets `{lint}` to allow; it is required at warn or deny"),
149            )),
150            Some(_) => {}
151            None => violations.push(violation(
152                ctx.file.rel,
153                section_line(ctx.file.lines, &section),
154                format!("[{section}] must enable `{lint}` at warn or deny"),
155            )),
156        }
157    }
158}
159
160fn lint_level(value: &toml::Value) -> Option<&str> {
161    match value {
162        toml::Value::String(level) => Some(level),
163        toml::Value::Table(table) => table.get("level").and_then(toml::Value::as_str),
164        _ => None,
165    }
166}
167
168fn member_violations(ctx: &TomlCtx<'_>, document: &toml::Table) -> Vec<Violation> {
169    let inherits = nested_table(document, &["lints"])
170        .and_then(|lints| lints.get("workspace"))
171        .and_then(toml::Value::as_bool)
172        == Some(true);
173
174    if inherits {
175        return Vec::new();
176    }
177
178    vec![violation(
179        ctx.file.rel,
180        section_line(ctx.file.lines, "lints"),
181        "member manifest must inherit workspace lints via `[lints] workspace = true`",
182    )]
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[gtest]
190    fn examples() -> Result<()> {
191        for example in EXAMPLES {
192            let rel = if example.code.contains("[workspace") {
193                "crates/Cargo.toml"
194            } else {
195                "crates/foo/Cargo.toml"
196            };
197            let violations = crate::test_support::check_source_toml_at(
198                rel,
199                example.code,
200                check_toml_cargo_workspace_lints,
201            );
202
203            verify_eq!(violations.is_empty(), example.pass)?;
204        }
205
206        Ok(())
207    }
208
209    #[gtest]
210    fn missing_workspace_lints_flags_every_required_entry() -> Result<()> {
211        let violations = crate::test_support::check_source_toml_at(
212            "crates/Cargo.toml",
213            "[workspace]\nmembers = []\n",
214            check_toml_cargo_workspace_lints,
215        );
216
217        verify_eq!(violations.len(), 35)?;
218
219        Ok(())
220    }
221
222    #[gtest]
223    fn non_cargo_toml_is_ignored() -> Result<()> {
224        let violations = crate::test_support::check_source_toml_at(
225            "crates/tidy.toml",
226            "[rules.x]\nenabled = true\n",
227            check_toml_cargo_workspace_lints,
228        );
229
230        verify_true!(violations.is_empty())?;
231
232        Ok(())
233    }
234}