wowlab_tidy/infra/
workspace.rs1use std::sync::{Arc, Mutex};
4
5use wowlab_fs::{
6 file,
7 path::{Path, PathBuf},
8 working_directory,
9};
10use wowlab_types::sim::FastMap;
11
12#[derive(Debug, Default)]
14pub(crate) struct WorkspaceMembers {
15 pub names: Vec<String>,
16 pub import_roots: Vec<String>,
17}
18
19#[derive(Debug, Default)]
21pub(crate) struct WorkspaceContext {
22 external_roots: Mutex<FastMap<PathBuf, Arc<[String]>>>,
23 members: Mutex<FastMap<PathBuf, Arc<WorkspaceMembers>>>,
24}
25
26impl WorkspaceContext {
27 pub(crate) fn members(&self, start: &Path) -> Arc<WorkspaceMembers> {
28 let root = workspace_root(start).unwrap_or_default();
29 let mut cache = self
30 .members
31 .lock()
32 .expect("workspace member cache poisoned");
33
34 Arc::clone(
35 cache
36 .entry(root.clone())
37 .or_insert_with(|| Arc::new(load_members(&root))),
38 )
39 }
40
41 pub(crate) fn external_dep_roots(&self, path: &Path) -> Option<Arc<[String]>> {
42 if !path.is_absolute() {
43 return None;
44 }
45
46 let crate_dir = owning_crate_dir(path)?;
47 let mut cache = self
48 .external_roots
49 .lock()
50 .expect("dependency root cache poisoned");
51
52 Some(Arc::clone(cache.entry(crate_dir.clone()).or_insert_with(
53 || load_external_deps(&crate_dir, self).into(),
54 )))
55 }
56}
57
58impl WorkspaceMembers {
59 pub(crate) fn is_member_root(&self, root: &str) -> bool {
60 self.import_roots.iter().any(|known| known == root)
61 }
62
63 pub(crate) fn is_member_package(&self, name: &str) -> bool {
64 self.names.iter().any(|known| known == name)
65 }
66}
67
68pub(crate) fn workspace_root(start: &Path) -> Option<PathBuf> {
70 let absolute = if start.is_absolute() {
71 start.to_path_buf()
72 } else {
73 working_directory::current().ok()?.join(start)
74 };
75
76 absolute
77 .ancestors()
78 .find(|dir| {
79 read_manifest(&dir.join("Cargo.toml"))
80 .is_some_and(|document| document.contains_key("workspace"))
81 })
82 .map(Path::to_path_buf)
83}
84
85pub(crate) fn member_manifests(root: &Path) -> Vec<PathBuf> {
87 let (files, _failures) =
88 wowlab_fs::walk::visible_source_files(root, &["toml"], ".tidyignore").into_parts();
89
90 files
91 .into_iter()
92 .filter(|path| {
93 path.file_name().is_some_and(|name| name == "Cargo.toml") && path.parent() != Some(root)
94 })
95 .collect()
96}
97
98fn owning_crate_dir(path: &Path) -> Option<PathBuf> {
99 path.ancestors()
100 .skip(1)
101 .find(|dir| {
102 read_manifest(&dir.join("Cargo.toml"))
103 .is_some_and(|document| document.contains_key("package"))
104 })
105 .map(Path::to_path_buf)
106}
107
108fn load_external_deps(crate_dir: &Path, workspace: &WorkspaceContext) -> Vec<String> {
109 let Some(document) = read_manifest(&crate_dir.join("Cargo.toml")) else {
110 return Vec::new();
111 };
112 let Some(dependencies) = document.get("dependencies").and_then(toml::Value::as_table) else {
113 return Vec::new();
114 };
115 let members = workspace.members(crate_dir);
116 let mut roots: Vec<String> = dependencies
117 .iter()
118 .filter(|(key, value)| {
119 let resolved = value
120 .as_table()
121 .and_then(|spec| spec.get("package"))
122 .and_then(toml::Value::as_str)
123 .unwrap_or(key);
124
125 !members.is_member_package(resolved)
126 })
127 .map(|(key, _)| key.replace('-', "_"))
128 .collect();
129
130 roots.sort();
131 roots.dedup();
132
133 roots
134}
135
136fn load_members(root: &Path) -> WorkspaceMembers {
137 let mut members = WorkspaceMembers::default();
138 let Some(document) = read_manifest(&root.join("Cargo.toml")) else {
139 return members;
140 };
141 let Some(entries) = document
142 .get("workspace")
143 .and_then(toml::Value::as_table)
144 .and_then(|workspace| workspace.get("members"))
145 .and_then(toml::Value::as_array)
146 else {
147 return members;
148 };
149
150 for dir_name in entries.iter().filter_map(toml::Value::as_str) {
151 let package = read_manifest(&root.join(dir_name).join("Cargo.toml"))
152 .and_then(|document| package_name(&document))
153 .unwrap_or_else(|| dir_name.to_owned());
154
155 push_unique(&mut members.import_roots, dir_name.replace('-', "_"));
156 push_unique(&mut members.import_roots, package.replace('-', "_"));
157 push_unique(&mut members.names, package);
158 }
159
160 members
161}
162
163fn package_name(document: &toml::Table) -> Option<String> {
164 document
165 .get("package")?
166 .as_table()?
167 .get("name")?
168 .as_str()
169 .map(String::from)
170}
171
172fn read_manifest(manifest: &Path) -> Option<toml::Table> {
173 let contents = file::read_text(manifest).ok()?;
174
175 toml::from_str(&contents).ok()
176}
177
178fn push_unique(values: &mut Vec<String>, value: String) {
179 if !values.contains(&value) {
180 values.push(value);
181 }
182}