mdwright_lint/stdlib/
heading_punctuation.rs1use crate::diagnostic::{Diagnostic, Fix};
8use crate::rule::LintRule;
9use mdwright_document::Document;
10
11pub struct HeadingPunctuation;
12
13impl LintRule for HeadingPunctuation {
14 fn name(&self) -> &str {
15 "heading-punctuation"
16 }
17
18 fn description(&self) -> &str {
19 "Trailing `.` or `:` on a heading."
20 }
21
22 fn explain(&self) -> &str {
23 include_str!("explain/heading_punctuation.md")
24 }
25
26 fn produces_fix(&self) -> bool {
27 true
28 }
29
30 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
31 for h in doc.headings() {
32 let last = h.text.chars().last();
33 let Some(c) = last else { continue };
34 if c != '.' && c != ':' {
35 continue;
36 }
37 let trailing_byte_len = c.len_utf8();
38 let local_start = h.text.len().saturating_sub(trailing_byte_len);
39 let local_end = h.text.len();
40 let message = format!("heading ends with `{c}` — headings should not carry sentence punctuation");
41 let fix = Some(Fix {
42 replacement: String::new(),
43 safe: true,
44 });
45 if let Some(d) = Diagnostic::at(doc, h.byte_offset, local_start..local_end, message, fix) {
46 out.push(d);
47 }
48 }
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use anyhow::Result;
55 use mdwright_document::Document;
56
57 use super::HeadingPunctuation;
58 use crate::apply_safe_fixes;
59 use crate::rule_set::RuleSet;
60
61 fn rules() -> Result<RuleSet> {
62 let mut rs = RuleSet::new();
63 rs.add(Box::new(HeadingPunctuation))
64 .map_err(|e| anyhow::anyhow!("{e}"))?;
65 Ok(rs)
66 }
67
68 #[test]
69 fn emits_safe_fix() -> Result<()> {
70 let doc = Document::parse("# Title.\n")?;
71 let diags = rules()?.check(&doc);
72 assert_eq!(diags.len(), 1);
73 let fix = diags
74 .first()
75 .and_then(|d| d.fix.as_ref())
76 .ok_or_else(|| anyhow::anyhow!("fix"))?;
77 assert!(fix.safe);
78 assert_eq!(fix.replacement, "");
79 Ok(())
80 }
81
82 #[test]
83 fn fix_strips_trailing_punctuation_and_is_idempotent() -> Result<()> {
84 let src = "# Intro:\n\n## Methods.\n";
85 let doc = Document::parse(src)?;
86 let diags = rules()?.check(&doc);
87 let (out, applied) = apply_safe_fixes(&doc, &diags);
88 assert_eq!(applied, 2);
89 assert_eq!(out, "# Intro\n\n## Methods\n");
90 let doc2 = Document::parse(&out)?;
91 assert!(rules()?.check(&doc2).is_empty());
92 Ok(())
93 }
94}