wowlab_tidy/languages/rust/rules/style/
doc_comment_period.rs1#[cfg(test)]
2use googletest::prelude::*;
3use winnow::combinator::alt;
4
5use crate::{Example, FileCtx, Violation, infra::parse, violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "missing period",
11 code: "/// Returns the value",
12 pass: false,
13 },
14 Example {
15 label: "with period",
16 code: "/// Returns the value.",
17 pass: true,
18 },
19 Example {
20 label: "markdown header",
21 code: "/// # Examples",
22 pass: true,
23 },
24 Example {
25 label: "empty doc comment",
26 code: "///",
27 pass: true,
28 },
29 Example {
30 label: "code fence",
31 code: "/// ```rust",
32 pass: true,
33 },
34 Example {
35 label: "inner doc missing period",
36 code: "//! Module description",
37 pass: false,
38 },
39 Example {
40 label: "inner doc with period",
41 code: "//! Module description.",
42 pass: true,
43 },
44 Example {
45 label: "ends with backtick",
46 code: "/// Returns `None`",
47 pass: true,
48 },
49 Example {
50 label: "ends with colon",
51 code: "/// The following:",
52 pass: true,
53 },
54];
55
56crate::line_rule!(
57 doc_comment_period,
58 "Require doc comments to end with proper punctuation.",
59 "Doc comments are sentences. Ending with punctuation keeps generated rustdoc consistent and professional. Not auto-fixable: appending a dot to a line that continues on the next line breaks the sentence — rephrase so each line is a complete sentence, or shorten the comment to one line.",
60 Low,
61);
62
63#[rustfmt::skip]
64const ALLOWED_ENDINGS: &[char] = &[
65 '.',
66 ')',
67 ':',
68 '!',
69 '?',
70 '`',
71 ']',
72];
73
74fn check_doc_comment_period(ctx: &FileCtx<'_>) -> Vec<Violation> {
75 let mut out = Vec::new();
76 let mut in_code_fence = false;
77
78 for (i, line) in ctx.lines.iter().enumerate() {
79 let lineno = i + 1;
80 let trimmed = line.trim();
81
82 let Some(raw) = parse::doc_comment_content(trimmed) else {
83 in_code_fence = false;
84 continue;
85 };
86 let text = raw.trim();
87
88 if parse::matches(text, "```") {
89 in_code_fence = !in_code_fence;
90 continue;
91 }
92
93 if in_code_fence || text.is_empty() {
94 continue;
95 }
96
97 if parse::matches(text, '#') {
98 continue;
99 }
100
101 if parse::matches(raw, alt((" ", " \t"))) {
102 continue;
103 }
104
105 if let Some(last) = text.chars().last() {
106 if !ALLOWED_ENDINGS.contains(&last) {
107 out.push(violation(
108 ctx.rel,
109 lineno,
110 "doc comment should end with `.`, `)`, `:`, `!`, `?`, `` ` ``, or `]` — do \
111 not just append a dot mid-sentence; rephrase so each line is a complete \
112 sentence, or shorten the comment to one line",
113 ));
114 }
115 }
116 }
117
118 out
119}
120
121crate::tidy_test!(check_doc_comment_period, {
122 crate::example_tests!(EXAMPLES, check_doc_comment_period);
123
124 #[gtest]
125 fn code_block_lines_are_skipped() -> Result<()> {
126 let src = "/// Example:\n/// ```\n/// let x = foo();\n/// assert_eq!(x, 1);\n/// ```";
127 let v = run(src);
128 verify_true!(v.is_empty())?;
129
130 Ok(())
131 }
132});