Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3
4use super::{
5    assignment_line, cargo_document, dependency_tables, is_workspace_member_manifest,
6    resolved_dependency_name,
7};
8use crate::{Example, TomlCtx, Violation, violation};
9
10const TEMPFILE_CRATE: &str = "tempfile";
11
12#[rustfmt::skip]
13const EXAMPLES: &[Example] = &[
14    Example { label: "ordinary dependency", code: "[dependencies]\nserde = \"1\"\n", pass: true },
15    Example { label: "no dependencies", code: "[package]\nname = \"consumer\"\n", pass: true },
16    Example { label: "direct dependency", code: "[dependencies]\ntempfile = \"3\"\n", pass: false },
17    Example { label: "dev dependency", code: "[dev-dependencies]\ntempfile = \"3\"\n", pass: false },
18    Example { label: "build dependency", code: "[build-dependencies]\ntempfile = \"3\"\n", pass: false },
19];
20
21crate::toml_rule!(
22    toml_cargo_tempfile_dependency,
23    "Ban direct tempfile dependencies outside the shared filesystem implementation.",
24    "Temporary-file lifecycle and cleanup policy belong to wowlab-fs; consumers must use its RAII temporary-file APIs instead of bypassing the filesystem boundary.",
25    High,
26);
27
28fn check_toml_cargo_tempfile_dependency(ctx: &TomlCtx<'_>) -> Vec<Violation> {
29    if !is_workspace_member_manifest(ctx.file.rel) {
30        return Vec::new();
31    }
32
33    let Some(document) = cargo_document(ctx) else {
34        return Vec::new();
35    };
36
37    let mut violations = Vec::new();
38
39    for table in dependency_tables(&document) {
40        for (key, value) in table {
41            if resolved_dependency_name(key, value) == TEMPFILE_CRATE {
42                violations.push(violation(
43                    ctx.file.rel,
44                    assignment_line(ctx.file.lines, key),
45                    "depends directly on tempfile; use the wowlab-fs temporary-file APIs",
46                ));
47            }
48        }
49    }
50
51    violations
52}
53
54#[cfg(test)]
55mod tests {
56    use std::collections::BTreeMap;
57
58    use wowlab_fs::path::Path;
59
60    use super::*;
61    use crate::{
62        FileCtx, Rule,
63        infra::config::{Config, RuleConfig},
64    };
65
66    fn run(source: &str) -> Vec<Violation> {
67        run_at("crates/consumer/Cargo.toml", source)
68    }
69
70    fn run_at(rel: &str, source: &str) -> Vec<Violation> {
71        crate::test_support::check_source_toml_at(rel, source, check_toml_cargo_tempfile_dependency)
72    }
73
74    fn analyze_with_ignore(rel: &str, source: &str, ignore: &[&str]) -> Vec<Violation> {
75        let mut config = Config::generate_default(&[]);
76
77        config.rules.insert(
78            "toml_cargo_tempfile_dependency".to_owned(),
79            RuleConfig {
80                enabled: true,
81                ignore: ignore.iter().map(|pattern| (*pattern).to_owned()).collect(),
82                params: BTreeMap::new(),
83            },
84        );
85        let lines: Vec<&str> = source.lines().collect();
86        let file = FileCtx {
87            rel,
88            path: Path::new(rel),
89            lines: &lines,
90            contents: source,
91            config: &config,
92        };
93        let rule = inventory::iter::<Rule>
94            .into_iter()
95            .find(|rule| rule.info.name == "toml_cargo_tempfile_dependency")
96            .expect("tempfile dependency rule must be registered");
97
98        crate::languages::toml::analyze(&file, &[rule], false).violations
99    }
100
101    crate::example_tests!(EXAMPLES, check_toml_cargo_tempfile_dependency);
102
103    #[gtest]
104    fn flags_renamed_dependency() -> Result<()> {
105        let violations = run_at(
106            "crates/consumer/Cargo.toml",
107            "[dependencies]\nscratch = { package = \"tempfile\", version = \"3\" }\n",
108        );
109
110        verify_eq!(violations.len(), 1)?;
111
112        Ok(())
113    }
114
115    #[gtest]
116    fn flags_target_specific_dependency() -> Result<()> {
117        let violations = run_at(
118            "crates/consumer/Cargo.toml",
119            "[target.'cfg(unix)'.dev-dependencies]\ntempfile = \"3\"\n",
120        );
121
122        verify_eq!(violations.len(), 1)?;
123
124        Ok(())
125    }
126
127    #[gtest]
128    fn workspace_root_declaration_is_allowed() -> Result<()> {
129        let violations = run_at(
130            "crates/Cargo.toml",
131            "[workspace.dependencies]\ntempfile = \"3\"\n",
132        );
133
134        verify_true!(violations.is_empty())?;
135
136        Ok(())
137    }
138
139    #[gtest]
140    fn filesystem_implementation_is_exempt_only_through_config() -> Result<()> {
141        let source = "[dependencies]\ntempfile = \"3\"\n";
142        let ignore = ["crates/fs/**"];
143
144        verify_true!(analyze_with_ignore("crates/fs/Cargo.toml", source, &ignore).is_empty())?;
145        verify_eq!(
146            analyze_with_ignore("crates/consumer/Cargo.toml", source, &ignore).len(),
147            1
148        )?;
149
150        Ok(())
151    }
152}