Skip to main content

wowlab_tidy/languages/rust/rules/docs/
module_docs.rs

1#[cfg(test)]
2use googletest::prelude::*;
3
4use crate::{Example, FileCtx, Violation, violation};
5
6/// Pass/fail cases checked against a gated `mod.rs` rel path.
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example {
10        label: "module docs on first line",
11        code: "//! Module docs.\n\npub fn f() {}",
12        pass: true,
13    },
14    Example {
15        label: "no module docs",
16        code: "pub fn f() {}",
17        pass: false,
18    },
19    Example {
20        label: "suppression directive before module docs",
21        code: "// #t(file: rust_panic) tooling fixture\n\n//! Module docs.\npub fn f() {}",
22        pass: true,
23    },
24    Example {
25        label: "inner attribute before module docs",
26        code: "#![allow(dead_code)]\n//! Module docs.\npub fn f() {}",
27        pass: true,
28    },
29    Example {
30        label: "multi-line inner attribute before module docs",
31        code: "#![allow(\n    dead_code\n)]\n//! Module docs.\npub fn f() {}",
32        pass: true,
33    },
34    Example {
35        label: "blank lines before module docs",
36        code: "\n\n//! Module docs.\npub fn f() {}",
37        pass: true,
38    },
39    Example {
40        label: "regular comment before module docs",
41        code: "// a stray comment\n//! Module docs.\npub fn f() {}",
42        pass: false,
43    },
44    Example {
45        label: "only items",
46        code: "pub struct S;",
47        pass: false,
48    },
49];
50
51crate::line_rule!(
52    module_docs,
53    "Require `//!` module docs at the top of `lib.rs` and `mod.rs` files.",
54    "Module docs are the entry point for API navigation; each module file should say what it contains (M-MODULE-DOCS).",
55    Medium,
56);
57
58fn check_module_docs(ctx: &FileCtx<'_>) -> Vec<Violation> {
59    if !is_module_entry(ctx.rel) {
60        return Vec::new();
61    }
62
63    let mut in_attr = false;
64
65    for line in ctx.lines {
66        let trimmed = line.trim();
67
68        if in_attr {
69            in_attr = !trimmed.ends_with(']');
70            continue;
71        }
72
73        if trimmed.starts_with("//!") {
74            return Vec::new();
75        }
76
77        if trimmed.is_empty() || trimmed.starts_with("// #t(") {
78            continue;
79        }
80
81        if trimmed.starts_with("#![") {
82            in_attr = !trimmed.ends_with(']');
83            continue;
84        }
85
86        break;
87    }
88
89    vec![violation(
90        ctx.rel,
91        1,
92        "`lib.rs`/`mod.rs` must begin with `//!` module docs",
93    )]
94}
95
96fn is_module_entry(rel: &str) -> bool {
97    if !rel.contains("/src/") {
98        return false;
99    }
100
101    let name = rel.rsplit('/').next().unwrap_or(rel);
102
103    name == "lib.rs" || name == "mod.rs"
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::test_support::check_source_at;
110
111    const GATED_REL: &str = "crates/demo/src/util/mod.rs";
112
113    #[gtest]
114    fn examples() -> Result<()> {
115        for ex in EXAMPLES {
116            let violations = check_source_at(GATED_REL, ex.code, check_module_docs);
117
118            verify_eq!(violations.is_empty(), ex.pass)?;
119        }
120
121        Ok(())
122    }
123
124    #[gtest]
125    fn lib_rs_is_gated() -> Result<()> {
126        let violations = check_source_at("crates/demo/src/lib.rs", "pub mod x;", check_module_docs);
127
128        verify_false!(violations.is_empty())?;
129
130        Ok(())
131    }
132
133    #[gtest]
134    fn ungated_paths_pass() -> Result<()> {
135        let rels = ["test.rs", "crates/demo/src/util.rs", "crates/demo/mod.rs"];
136
137        for rel in rels {
138            let violations = check_source_at(rel, "pub fn f() {}", check_module_docs);
139
140            verify_true!(violations.is_empty())?;
141        }
142
143        Ok(())
144    }
145}