Skip to main content

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

1// #t(file: rust_default_hasher) small per-file section set; fast-hasher dependency not warranted
2
3use std::collections::HashSet;
4
5use super::MANIFEST_PREFIX;
6use crate::{Example, TomlCtx, Violation, violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example { label: "contiguous sections", code: "schema_version = 1\n\n[spec]\nid = 1\n\n[auras.ONE]\nid = 2\n\n[auras.TWO]\nid = 3\n\n[spells.ONE]\nid = 4\n", pass: true },
11    Example { label: "aura section resumes", code: "schema_version = 1\n\n[spec]\nid = 1\n\n[auras.ONE]\nid = 2\n\n[spells.ONE]\nid = 3\n\n[auras.TWO]\nid = 4\n", pass: false },
12];
13
14crate::toml_rule!(
15    toml_manifest_section_contiguity,
16    "Require each top-level manifest section to occupy one contiguous region.",
17    "Returning to auras, spells, or another section later in a file hides append-only fragments and makes declaration order difficult to review.",
18    Medium,
19);
20
21fn check_toml_manifest_section_contiguity(ctx: &TomlCtx<'_>) -> Vec<Violation> {
22    if !ctx.file.rel.starts_with(MANIFEST_PREFIX) || !ctx.parse.errors.is_empty() {
23        return Vec::new();
24    }
25
26    let mut closed = HashSet::new();
27    let mut previous: Option<&str> = None;
28    let mut violations = Vec::new();
29
30    for (index, line) in ctx.file.lines.iter().enumerate() {
31        let Some(section) = top_level_section(line.trim()) else {
32            continue;
33        };
34
35        if previous == Some(section) {
36            continue;
37        }
38
39        if let Some(previous) = previous {
40            closed.insert(previous);
41        }
42
43        if closed.contains(section) {
44            violations.push(violation(
45                ctx.file.rel,
46                index + 1,
47                format!("section `{section}` resumes after another top-level section"),
48            ));
49        }
50
51        previous = Some(section);
52    }
53
54    violations
55}
56
57fn top_level_section(header: &str) -> Option<&str> {
58    let inner = header
59        .strip_prefix("[[")
60        .and_then(|value| value.strip_suffix("]]"))
61        .or_else(|| {
62            header
63                .strip_prefix('[')
64                .and_then(|value| value.strip_suffix(']'))
65        })?;
66
67    inner.split('.').next()
68}
69
70crate::tidy_toml_test!(check_toml_manifest_section_contiguity, {
71    crate::example_tests!(EXAMPLES, check_toml_manifest_section_contiguity);
72});