wowlab_tidy/languages/toml/rules/cargo/
unused_deps.rs1#[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 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}