wowlab_tidy/languages/rust/rules/docs/
module_docs.rs1#[cfg(test)]
2use googletest::prelude::*;
3
4use crate::{Example, FileCtx, Violation, violation};
5
6#[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}