wowlab_tidy/languages/toml/rules/manifest/
comment_blocks.rs1#[cfg(test)]
2use googletest::prelude::*;
3
4use super::MANIFEST_PREFIX;
5use crate::{Example, Fix, TomlCtx, Violation, violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "single-line comment",
11 code: "# One complete comment.\nvalue = 1\n",
12 pass: true,
13 },
14 Example {
15 label: "consecutive comment lines",
16 code: "# One comment that\n# continues here.\nvalue = 1\n",
17 pass: false,
18 },
19 Example {
20 label: "comments separated by a blank line",
21 code: "# First comment.\n\n# Second comment.\nvalue = 1\n",
22 pass: true,
23 },
24 Example {
25 label: "inline comments",
26 code: "first = 1 # First comment.\nsecond = 2 # Second comment.\n",
27 pass: true,
28 },
29 Example {
30 label: "decorative banner",
31 code: "# -------- Rotation schema --------\nvalue = 1\n",
32 pass: false,
33 },
34];
35
36crate::toml_rule!(
37 toml_manifest_comment_blocks,
38 "Require concise, single-line standalone comments in engine manifests.",
39 "Single-line comments without decorative banners remain locally readable and avoid inconsistent hand-wrapped prose.",
40 Low,
41 fix_toml_manifest_comment_blocks,
42);
43
44fn check_toml_manifest_comment_blocks(ctx: &TomlCtx<'_>) -> Vec<Violation> {
45 if !ctx.file.rel.starts_with(MANIFEST_PREFIX) || !ctx.parse.errors.is_empty() {
46 return Vec::new();
47 }
48
49 comment_runs(ctx.file.lines)
50 .filter(|(start, end)| {
51 end - start + 1 > 1
52 || ctx
53 .file
54 .line(*start)
55 .and_then(standalone_comment_content)
56 .is_some_and(is_decorative_banner)
57 })
58 .map(|(start, end)| {
59 violation(
60 ctx.file.rel,
61 start,
62 if start == end {
63 "decorative comment banner".to_owned()
64 } else {
65 format!("standalone comment spans lines {start}-{end}")
66 },
67 )
68 })
69 .collect()
70}
71
72fn fix_toml_manifest_comment_blocks(ctx: &TomlCtx<'_>, violation: &Violation) -> Option<Fix> {
73 let start = violation.line;
74 let (_, end) = comment_runs(ctx.file.lines).find(|(line, _)| *line == start)?;
75 let first = *ctx.file.lines.get(start.checked_sub(1)?)?;
76 let indent = first.get(..first.len() - first.trim_start().len())?;
77 let content = ctx
78 .file
79 .lines
80 .get(start - 1..end)?
81 .iter()
82 .filter_map(|line| standalone_comment_content(line))
83 .filter(|part| !part.is_empty())
84 .collect::<Vec<_>>()
85 .join(" ");
86 let content = canonical_comment_content(&content);
87
88 Some(Fix {
89 start_line: start,
90 end_line: end,
91 replacement: format!("{indent}#{}", if content.is_empty() { "" } else { " " }) + &content,
92 })
93}
94
95fn comment_runs<'a>(lines: &'a [&'a str]) -> impl Iterator<Item = (usize, usize)> + 'a {
96 let mut line = 0;
97
98 std::iter::from_fn(move || {
99 loop {
100 while lines
101 .get(line)
102 .is_some_and(|current| standalone_comment_content(current).is_none())
103 {
104 line += 1;
105 }
106
107 let start = line;
108
109 while lines
110 .get(line)
111 .is_some_and(|current| standalone_comment_content(current).is_some())
112 {
113 line += 1;
114 }
115
116 if line > start {
117 return Some((start + 1, line));
118 }
119
120 if line == lines.len() {
121 return None;
122 }
123 }
124 })
125}
126
127fn canonical_comment_content(content: &str) -> String {
128 const BANNER_RUN_LENGTH: usize = 3;
129
130 let mut output = String::with_capacity(content.len());
131 let mut characters = content.trim().chars().peekable();
132
133 while let Some(character) = characters.next() {
134 if character != '-' {
135 output.push(character);
136 continue;
137 }
138
139 let mut count = 1;
140
141 while characters.next_if_eq(&'-').is_some() {
142 count += 1;
143 }
144
145 if count < BANNER_RUN_LENGTH {
146 output.extend(std::iter::repeat_n('-', count));
147 continue;
148 }
149
150 while output.chars().last().is_some_and(char::is_whitespace) {
151 output.pop();
152 }
153
154 while characters.next_if(|next| next.is_whitespace()).is_some() {}
155
156 if !output.is_empty() && characters.peek().is_some() {
157 if !output
158 .chars()
159 .last()
160 .is_some_and(|last| matches!(last, '.' | ':' | ';' | '!' | '?'))
161 {
162 output.push('.');
163 }
164
165 output.push(' ');
166 }
167 }
168
169 output.trim().to_owned()
170}
171
172fn is_decorative_banner(content: &str) -> bool {
173 content.contains("---")
174}
175
176fn standalone_comment_content(line: &str) -> Option<&str> {
177 line.trim_start().strip_prefix('#').map(str::trim)
178}
179
180crate::tidy_toml_test!(check_toml_manifest_comment_blocks, {
181 crate::example_tests!(EXAMPLES, check_toml_manifest_comment_blocks);
182
183 #[gtest]
184 fn fixer_merges_a_comment_block() -> Result<()> {
185 let fixed = crate::apply_toml_fixes(
186 "# One comment that\n# continues here.\nvalue = 1\n",
187 check_toml_manifest_comment_blocks,
188 fix_toml_manifest_comment_blocks,
189 );
190 verify_eq!(fixed, "# One comment that continues here.\nvalue = 1")?;
191
192 Ok(())
193 }
194
195 #[gtest]
196 fn fixer_preserves_indentation() -> Result<()> {
197 let fixed = crate::apply_toml_fixes(
198 " # One comment that\n # continues here.\nvalue = 1\n",
199 check_toml_manifest_comment_blocks,
200 fix_toml_manifest_comment_blocks,
201 );
202 verify_eq!(fixed, " # One comment that continues here.\nvalue = 1")?;
203
204 Ok(())
205 }
206
207 #[gtest]
208 fn checker_continues_after_a_single_line_comment() -> Result<()> {
209 let violations = run("# First comment.\nvalue = 1\n# Wrapped\n# comment.\n");
210 verify_eq!(violations.len(), 1)?;
211 verify_eq!(violations[0].line, 3)?;
212
213 Ok(())
214 }
215
216 #[gtest]
217 fn fixer_removes_decorative_banner() -> Result<()> {
218 let fixed = crate::apply_toml_fixes(
219 "# -------- Rotation schema --------\nvalue = 1\n",
220 check_toml_manifest_comment_blocks,
221 fix_toml_manifest_comment_blocks,
222 );
223 verify_eq!(fixed, "# Rotation schema\nvalue = 1")?;
224
225 Ok(())
226 }
227
228 #[gtest]
229 fn fixer_replaces_an_internal_barrel_with_punctuation() -> Result<()> {
230 let fixed = crate::apply_toml_fixes(
231 "# Rotation schema -------- Fields exposed to the UI.\nvalue = 1\n",
232 check_toml_manifest_comment_blocks,
233 fix_toml_manifest_comment_blocks,
234 );
235 verify_eq!(
236 fixed,
237 "# Rotation schema. Fields exposed to the UI.\nvalue = 1"
238 )?;
239
240 Ok(())
241 }
242});