Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_types::sim::FastSet;
4
5use crate::{Example, Violation, languages::workspace::WorkspaceCtx, matches_ignore, violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example { label: "unused dependency", code: "fn main() {}", pass: false },
10    Example { label: "path-referenced dependency", code: "fn encode(value: serde::Value) {}", pass: true },
11    Example { label: "derive-only dependency", code: "#[derive(serde::Serialize)] struct Value;", pass: true },
12];
13
14crate::language_workspace_rule!(
15    toml_cargo_unused_deps,
16    "Flag workspace-member dependencies that are never referenced by any Rust compile target.",
17    "Unused dependencies increase build time and supply-chain surface. Full-workspace runs are authoritative because references are aggregated across src, tests, benches, examples, and build.rs.",
18    Medium,
19    params {
20        always_used: [String] = [],
21    },
22);
23
24fn check_toml_cargo_unused_deps(ctx: &WorkspaceCtx<'_>) -> Vec<Violation> {
25    let always_used: FastSet<String> = ctx
26        .config
27        .get_str_array("toml_cargo_unused_deps", &PARAMS[0])
28        .into_iter()
29        .map(|name| name.replace('-', "_"))
30        .collect();
31    let mut violations = Vec::new();
32
33    for manifest in ctx.manifests {
34        if matches_ignore(
35            &manifest.rel,
36            ctx.config.ignore_patterns("toml_cargo_unused_deps"),
37        ) {
38            continue;
39        }
40
41        let Some((member_dir, _)) = manifest.rel.rsplit_once('/') else {
42            continue;
43        };
44        // #t(rust_alloc_in_loop) each workspace member needs its own source-path prefix
45        let prefix = format!("{member_dir}/");
46        let referenced: FastSet<&str> = ctx
47            .files
48            .iter()
49            .filter(|file| file.rel.starts_with(&prefix))
50            .flat_map(|file| file.crate_roots.iter().map(String::as_str))
51            .collect();
52
53        for dependency in &manifest.dependencies {
54            if always_used.contains(&dependency.root)
55                || referenced.contains(dependency.root.as_str())
56            {
57                continue;
58            }
59
60            violations.push(violation(
61                &manifest.rel,
62                dependency.line,
63                format!(
64                    "dependency `{}` is never referenced by any Rust target in this workspace member",
65                    dependency.name
66                ),
67            ));
68        }
69    }
70
71    violations
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn run(source: &str, dependency_name: &str) -> Vec<Violation> {
79        crate::test_support::check_workspace_member(
80            "crates/example",
81            source,
82            &[(dependency_name, dependency_name)],
83            check_toml_cargo_unused_deps,
84        )
85    }
86
87    #[gtest]
88    fn examples() -> Result<()> {
89        for example in EXAMPLES {
90            let violations = run(example.code, "serde");
91
92            verify_eq!(violations.is_empty(), example.pass)?;
93        }
94
95        Ok(())
96    }
97
98    #[gtest]
99    fn renamed_dependency_uses_the_manifest_key_as_its_crate_root() -> Result<()> {
100        let violations = run("fn decode(value: json::Value) {}", "json");
101
102        verify_true!(violations.is_empty())?;
103
104        Ok(())
105    }
106
107    #[gtest]
108    fn qualified_path_inside_macro_tokens_counts_as_used() -> Result<()> {
109        let violations = run(
110            "macro_rules! encode { () => { serde::Serialize }; }",
111            "serde",
112        );
113
114        verify_true!(violations.is_empty())?;
115
116        Ok(())
117    }
118}