wowlab_tidy/languages/toml/rules/cargo/
flat_layout.rs1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_fs::{file, path::Path};
4
5use super::cargo_document;
6use crate::{Example, TomlCtx, Violation, violation};
7
8const NON_DIRECTORY_COMPONENTS: usize = 2;
10
11#[rustfmt::skip]
12const EXAMPLES: &[Example] = &[
13 Example { label: "crate directly under crates/", code: "[package]\nname = \"foo\"\n", pass: true },
14 Example { label: "workspace manifest without package", code: "[workspace]\nmembers = [\"foo\"]\n", pass: true },
15 Example { label: "crate nested two levels deep", code: "[package]\nname = \"bar\"\n", pass: false },
16];
17
18#[cfg(test)]
19const EXAMPLE_RELS: &[&str] = &[
20 "crates/foo/Cargo.toml",
21 "crates/Cargo.toml",
22 "crates/foo/bar/Cargo.toml",
23];
24
25crate::toml_rule!(
26 toml_cargo_flat_layout,
27 "Require every crate to be a direct child of crates/ and never nested inside another crate.",
28 "A crate inside another crate's tree breaks the standard flat layout, confuses tooling, and is never acceptable (M-CRATES-FLAT-FOLDER).",
29 High,
30);
31
32fn check_toml_cargo_flat_layout(ctx: &TomlCtx<'_>) -> Vec<Violation> {
33 if !ctx.file.rel.starts_with("crates/")
34 || Path::new(ctx.file.rel)
35 .file_name()
36 .is_none_or(|name| name != "Cargo.toml")
37 {
38 return Vec::new();
39 }
40
41 let Some(document) = cargo_document(ctx) else {
42 return Vec::new();
43 };
44
45 if !document.contains_key("package") {
46 return Vec::new();
47 }
48
49 let mut violations = depth_violations(ctx.file.rel);
50
51 violations.extend(nested_crate_violations(ctx.file.rel, ctx.file.path));
52
53 violations
54}
55
56fn depth_violations(rel: &str) -> Vec<Violation> {
57 let depth = Path::new(rel)
58 .components()
59 .count()
60 .saturating_sub(NON_DIRECTORY_COMPONENTS);
61
62 if depth <= 1 {
63 return Vec::new();
64 }
65
66 vec![violation(
67 rel,
68 1,
69 format!(
70 "crate manifest sits {depth} directory levels below crates/; crates must be direct children of crates/"
71 ),
72 )]
73}
74
75fn nested_crate_violations(rel: &str, path: &Path) -> Vec<Violation> {
76 let mut violations = Vec::new();
77 let Some(crate_dir) = path.parent() else {
78 return violations;
79 };
80
81 for ancestor in crate_dir.ancestors().skip(1) {
82 if ancestor.file_name().is_none_or(|name| name == "crates") {
83 break;
84 }
85
86 let Ok(contents) = file::read_text(&ancestor.join("Cargo.toml")) else {
87 continue;
88 };
89 let encloses_crate = toml::from_str::<toml::Table>(&contents)
90 .is_ok_and(|document| document.contains_key("package"));
91
92 if encloses_crate {
93 violations.push(violation(
94 rel,
95 1,
96 format!(
97 "crate is nested inside crate `{}`; a crate must never contain another crate",
98 ancestor.display()
99 ),
100 ));
101 }
102 }
103
104 violations
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[gtest]
112 fn examples() -> Result<()> {
113 verify_eq!(EXAMPLES.len(), EXAMPLE_RELS.len())?;
114
115 for (example, rel) in EXAMPLES.iter().zip(EXAMPLE_RELS) {
116 let violations = crate::test_support::check_source_toml_at(
117 rel,
118 example.code,
119 check_toml_cargo_flat_layout,
120 );
121
122 verify_eq!(violations.is_empty(), example.pass)?;
123 }
124
125 Ok(())
126 }
127
128 #[gtest]
129 fn crate_nested_inside_crate_is_flagged() -> Result<()> {
130 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
131 let root = directory.path();
132 let outer = root.join("crates").join("outer");
133 let inner = outer.join("src").join("inner");
134
135 wowlab_fs::directory::ensure(&inner).or_fail()?;
136 file::write_text(&outer.join("Cargo.toml"), "[package]\nname = \"outer\"\n").or_fail()?;
137 let inner_manifest = inner.join("Cargo.toml");
138
139 file::write_text(&inner_manifest, "[package]\nname = \"inner\"\n").or_fail()?;
140
141 let violations =
142 nested_crate_violations("crates/outer/src/inner/Cargo.toml", &inner_manifest);
143
144 verify_eq!(violations.len(), 1)?;
145 verify_true!(violations[0].message.contains("outer"))?;
146
147 Ok(())
148 }
149}