Skip to main content

wowlab_tidy/languages/toml/rules/manifest/
comment_blocks.rs

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