Skip to main content

wowlab_tidy/languages/rust/rules/style/
doc_comment_period.rs

1#[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});