Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_fs::{file, path::Path};
4
5use super::cargo_document;
6use crate::{Example, TomlCtx, Violation, violation};
7
8/// `crates/` prefix plus the `Cargo.toml` file name.
9const 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}