Skip to main content

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

1// #t(file: rust_default_hasher, rust_missing_capacity) manifest-id maps on a cold audit path; hashing and preallocation are not bottlenecks
2
3use std::collections::{HashMap, HashSet};
4
5use wowlab_fs::{
6    directory, file,
7    path::{Path, PathBuf},
8};
9use wowlab_manifest_schema::ManifestRepository;
10
11use super::{ITEMS_REL, manifest_repository_root, workspace_relative};
12use crate::{Example, TomlCtx, Violation, violation};
13
14const EXAMPLES: &[Example] = &[];
15
16crate::toml_rule!(
17    toml_manifest_repository,
18    "Require a closed, unambiguous manifest component graph and unique spec IDs.",
19    "Missing, duplicate, or orphaned components make filesystem structure diverge from the manifests actually composed at build time.",
20    High,
21);
22
23fn check_toml_manifest_repository(ctx: &TomlCtx<'_>) -> Vec<Violation> {
24    if ctx.file.rel != ITEMS_REL || !ctx.parse.errors.is_empty() {
25        return Vec::new();
26    }
27
28    let Some(root) = manifest_repository_root(ctx.file.path) else {
29        return Vec::new();
30    };
31    let repository = ManifestRepository::new(root);
32    let spec_paths = match repository.spec_paths() {
33        Ok(paths) => paths,
34        Err(error) => return vec![violation(ctx.file.rel, 1, error.to_string())],
35    };
36    let mut violations = Vec::new();
37    let mut ids: HashMap<u32, &Path> = HashMap::new();
38    let mut referenced = HashSet::new();
39
40    for path in &spec_paths {
41        match repository.load_spec(path) {
42            Ok(manifest) => {
43                if let Some(previous) = ids.insert(manifest.spec.id, path) {
44                    let rel = workspace_relative(path);
45
46                    violations.push(violation(
47                        &rel,
48                        1,
49                        format!(
50                            "spec id {} duplicates {}",
51                            manifest.spec.id,
52                            workspace_relative(previous)
53                        ),
54                    ));
55                }
56            }
57            Err(error) => {
58                let rel = workspace_relative(path);
59
60                violations.push(violation(&rel, 1, error.to_string()));
61            }
62        }
63
64        collect_component_references(path, &mut referenced);
65    }
66
67    match repository.toml_paths() {
68        Ok(paths) => {
69            for path in paths {
70                if is_component(&path) {
71                    let rel = workspace_relative(&path);
72                    let identity = directory::canonicalize(&path).unwrap_or(path);
73
74                    if !referenced.contains(&identity) {
75                        violations.push(violation(
76                            &rel,
77                            1,
78                            "manifest component is not referenced by any spec",
79                        ));
80                    }
81                }
82            }
83        }
84        Err(error) => violations.push(violation(ctx.file.rel, 1, error.to_string())),
85    }
86
87    violations
88}
89
90fn collect_component_references(manifest_path: &Path, referenced: &mut HashSet<PathBuf>) {
91    let Ok(source) = file::read_text(manifest_path) else {
92        return;
93    };
94    let Ok(document) = toml::from_str::<toml::Table>(&source) else {
95        return;
96    };
97    let Some(spec_dir) = manifest_path.parent() else {
98        return;
99    };
100
101    if let Some(parts) = document.get("parts").and_then(toml::Value::as_array) {
102        for part in parts.iter().filter_map(toml::Value::as_str) {
103            let path = spec_dir.join(part);
104
105            referenced.insert(directory::canonicalize(&path).unwrap_or(path));
106        }
107    }
108
109    let Some(class_dir) = spec_dir.parent() else {
110        return;
111    };
112
113    if let Some(shared) = document.get("shared").and_then(toml::Value::as_array) {
114        for include in shared.iter().filter_map(toml::Value::as_table) {
115            let Some(relative) = include.get("path").and_then(toml::Value::as_str) else {
116                continue;
117            };
118            let path = class_dir.join("shared").join(relative);
119
120            referenced.insert(directory::canonicalize(&path).unwrap_or(path));
121        }
122    }
123}
124
125fn is_component(path: &Path) -> bool {
126    path.file_name()
127        .is_some_and(|name| name != "manifest.toml" && name != "items.toml")
128        && path
129            .components()
130            .any(|component| component.as_os_str() == "specs")
131}