Skip to main content

wowlab_tidy/languages/rust/rules/correctness/
docref.rs

1use crate::{Example, FileCtx, Violation, infra::parse, violation};
2
3#[rustfmt::skip]
4const EXAMPLES: &[Example] = &[
5    Example {
6        label: "valid start/end pair",
7        code: "// docref:start chunk-seed\nlet x = 1;\n// docref:end chunk-seed",
8        pass: true,
9    },
10    Example {
11        label: "no markers",
12        code: "let x = 1;\n\nlet y = 2;",
13        pass: true,
14    },
15    Example {
16        label: "start without end",
17        code: "// docref:start foo\nlet x = 1;",
18        pass: false,
19    },
20    Example {
21        label: "end without start",
22        code: "let x = 1;\n// docref:end foo",
23        pass: false,
24    },
25    Example {
26        label: "mismatched ids",
27        code: "// docref:start foo\nlet x = 1;\n// docref:end bar",
28        pass: false,
29    },
30    Example {
31        label: "missing id",
32        code: "// docref:start\nlet x = 1;\n// docref:end",
33        pass: false,
34    },
35    Example {
36        label: "non-kebab id",
37        code: "// docref:start Foo_Bar\nlet x = 1;\n// docref:end Foo_Bar",
38        pass: false,
39    },
40    Example {
41        label: "nested starts",
42        code: "// docref:start a\n// docref:start b\nlet x = 1;\n// docref:end b\n// docref:end a",
43        pass: false,
44    },
45    Example {
46        label: "blank line inside region",
47        code: "// docref:start foo\nlet x = 1;\n\nlet y = 2;\n// docref:end foo",
48        pass: false,
49    },
50    Example {
51        label: "duplicate id",
52        code: "// docref:start foo\nlet x = 1;\n// docref:end foo\n// docref:start foo\nlet y = 2;\n// docref:end foo",
53        pass: false,
54    },
55];
56
57crate::line_rule!(
58    docref,
59    "Validate `// docref:start`/`// docref:end` code-embed markers (pairing, ids, no blank lines inside).",
60    "Malformed or unbalanced markers break the bible code-embed loader and ship stale or missing source into the docs.",
61    High,
62);
63
64enum MarkerCheck {
65    Malformed(String),
66    Marker { id: String, is_start: bool },
67}
68
69fn is_kebab_id(id: &str) -> bool {
70    !id.is_empty()
71        && id
72            .bytes()
73            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
74}
75
76fn classify_marker(content: &str) -> Option<MarkerCheck> {
77    let body = content.trim().strip_prefix("docref:")?;
78
79    let (kind, id) = match body.split_once(' ') {
80        Some((kind, rest)) => (kind, rest.trim()),
81        None => (body, ""),
82    };
83
84    let is_start = match kind {
85        "start" => true,
86        "end" => false,
87        _ => {
88            return Some(MarkerCheck::Malformed(format!(
89                "unknown docref kind `{kind}` (expected `start` or `end`)"
90            )));
91        }
92    };
93
94    if id.is_empty() {
95        return Some(MarkerCheck::Malformed(format!(
96            "docref:{kind} marker has no id"
97        )));
98    }
99
100    if !is_kebab_id(id) {
101        return Some(MarkerCheck::Malformed(format!(
102            "docref id `{id}` must be kebab-case [a-z0-9-]"
103        )));
104    }
105
106    Some(MarkerCheck::Marker {
107        id: id.to_string(),
108        is_start,
109    })
110}
111
112fn check_docref(ctx: &FileCtx<'_>) -> Vec<Violation> {
113    let mut out = Vec::new();
114    let mut open: Option<(String, usize)> = None;
115    let mut closed: Vec<String> = Vec::new();
116
117    for (i, line) in ctx.lines.iter().enumerate() {
118        let lineno = i + 1;
119
120        match parse::prose_comment_content(line).and_then(classify_marker) {
121            Some(MarkerCheck::Malformed(msg)) => out.push(violation(ctx.rel, lineno, msg)),
122            Some(MarkerCheck::Marker { id, is_start: true }) => {
123                if let Some((open_id, open_line)) = open.as_ref() {
124                    out.push(violation(
125                        ctx.rel,
126                        lineno,
127                        format!("docref:start `{id}` nested inside `{open_id}` (opened at line {open_line})"),
128                    ));
129                } else {
130                    open = Some((id, lineno));
131                }
132            }
133            Some(MarkerCheck::Marker {
134                id,
135                is_start: false,
136            }) => match open.take() {
137                None => out.push(violation(
138                    ctx.rel,
139                    lineno,
140                    format!("docref:end `{id}` has no matching docref:start"),
141                )),
142                Some((open_id, _)) if open_id != id => out.push(violation(
143                    ctx.rel,
144                    lineno,
145                    format!("docref:end `{id}` does not match open docref:start `{open_id}`"),
146                )),
147                Some((open_id, _)) => {
148                    if closed.contains(&open_id) {
149                        out.push(violation(
150                            ctx.rel,
151                            lineno,
152                            format!("duplicate docref id `{open_id}`"),
153                        ));
154                    } else {
155                        closed.push(open_id);
156                    }
157                }
158            },
159            None => {
160                if open.is_some() && line.trim().is_empty() {
161                    out.push(violation(
162                        ctx.rel,
163                        lineno,
164                        "blank line inside docref region (markers must wrap contiguous code)",
165                    ));
166                }
167            }
168        }
169    }
170
171    if let Some((open_id, open_line)) = open {
172        out.push(violation(
173            ctx.rel,
174            open_line,
175            format!("docref:start `{open_id}` has no matching docref:end"),
176        ));
177    }
178
179    out
180}
181
182crate::tidy_test!(check_docref, {
183    crate::example_tests!(EXAMPLES, check_docref);
184});