wowlab_tidy/languages/rust/rules/correctness/
docref.rs1use 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});