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