1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt,
4};
5
6use crate::{PackageId, PackageSnapshot};
7
8#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct WorkspaceGraph {
11 packages: BTreeMap<PackageId, PackageSnapshot>,
12 dependencies: BTreeMap<PackageId, BTreeSet<PackageId>>,
13}
14
15impl WorkspaceGraph {
16 pub fn new(packages: Vec<PackageSnapshot>) -> Result<Self, WorkspaceGraphError> {
18 let mut package_map = BTreeMap::new();
19
20 for package in packages {
21 let id = package.id.clone();
22 if package_map.insert(id.clone(), package).is_some() {
23 return Err(WorkspaceGraphError::DuplicatePackageId { package: id });
24 }
25 }
26
27 let mut dependencies = BTreeMap::new();
28 for (id, package) in &package_map {
29 let mut package_dependencies = BTreeSet::new();
30 for dependency in &package.dependencies {
31 if !package_map.contains_key(&dependency.package) {
32 return Err(WorkspaceGraphError::UnknownDependency {
33 package: id.clone(),
34 dependency: dependency.package.clone(),
35 });
36 }
37 package_dependencies.insert(dependency.package.clone());
38 }
39 dependencies.insert(id.clone(), package_dependencies);
40 }
41
42 Ok(Self {
43 packages: package_map,
44 dependencies,
45 })
46 }
47
48 #[must_use]
49 pub fn package(&self, id: &PackageId) -> Option<&PackageSnapshot> {
50 self.packages.get(id)
51 }
52
53 pub fn packages(&self) -> impl Iterator<Item = &PackageSnapshot> {
54 self.packages.values()
55 }
56
57 pub fn topological_order(&self) -> Result<Vec<PackageId>, WorkspaceGraphError> {
59 let mut remaining_dependencies = self.dependencies.clone();
60 let mut order = Vec::with_capacity(self.packages.len());
61
62 while let Some(id) = remaining_dependencies
63 .iter()
64 .find_map(|(id, dependencies)| dependencies.is_empty().then(|| id.clone()))
65 {
66 remaining_dependencies.remove(&id);
67 for dependencies in remaining_dependencies.values_mut() {
68 dependencies.remove(&id);
69 }
70 order.push(id);
71 }
72
73 if remaining_dependencies.is_empty() {
74 Ok(order)
75 } else {
76 Err(WorkspaceGraphError::DependencyCycle {
77 cycle: self.find_cycle(&remaining_dependencies)?,
78 })
79 }
80 }
81
82 fn find_cycle(
83 &self,
84 remaining_dependencies: &BTreeMap<PackageId, BTreeSet<PackageId>>,
85 ) -> Result<Vec<PackageId>, WorkspaceGraphError> {
86 let mut visited = BTreeSet::new();
87 let mut stack = Vec::new();
88 let mut visiting = BTreeSet::new();
89
90 for id in remaining_dependencies.keys() {
91 if let Some(cycle) = Self::visit(
92 id,
93 remaining_dependencies,
94 &mut visited,
95 &mut visiting,
96 &mut stack,
97 ) {
98 return Ok(cycle);
99 }
100 }
101
102 Err(WorkspaceGraphError::CycleDetectionFailed)
103 }
104
105 fn visit(
106 id: &PackageId,
107 remaining_dependencies: &BTreeMap<PackageId, BTreeSet<PackageId>>,
108 visited: &mut BTreeSet<PackageId>,
109 visiting: &mut BTreeSet<PackageId>,
110 stack: &mut Vec<PackageId>,
111 ) -> Option<Vec<PackageId>> {
112 if let Some(cycle_start) = stack.iter().position(|package| package == id) {
113 let mut cycle = stack.iter().skip(cycle_start).cloned().collect::<Vec<_>>();
114 cycle.push(id.clone());
115 return Some(cycle);
116 }
117 if !visited.insert(id.clone()) {
118 return None;
119 }
120
121 visiting.insert(id.clone());
122 stack.push(id.clone());
123 let dependencies = remaining_dependencies.get(id)?;
124 for dependency in dependencies {
125 if let Some(cycle) =
126 Self::visit(dependency, remaining_dependencies, visited, visiting, stack)
127 {
128 return Some(cycle);
129 }
130 }
131 stack.pop();
132 visiting.remove(id);
133 None
134 }
135}
136
137#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
139pub enum WorkspaceGraphError {
140 #[error("duplicate package id: {package}")]
141 DuplicatePackageId { package: PackageId },
142 #[error("package {package} depends on unknown package {dependency}")]
143 UnknownDependency {
144 package: PackageId,
145 dependency: PackageId,
146 },
147 #[error("dependency cycle: {}", display_cycle(.cycle))]
148 DependencyCycle { cycle: Vec<PackageId> },
149 #[error("failed to identify a dependency cycle in the remaining graph")]
150 CycleDetectionFailed,
151}
152
153fn display_cycle(cycle: &[PackageId]) -> DependencyCycleDisplay<'_> {
154 DependencyCycleDisplay(cycle)
155}
156
157struct DependencyCycleDisplay<'a>(&'a [PackageId]);
158
159impl fmt::Display for DependencyCycleDisplay<'_> {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 for (index, package) in self.0.iter().enumerate() {
162 if index > 0 {
163 formatter.write_str(" -> ")?;
164 }
165 package.fmt(formatter)?;
166 }
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use camino::Utf8PathBuf;
174 use semver::Version;
175
176 use super::*;
177 use crate::{Dependency, DependencyKind, DependencySource, EcosystemId, VersionSource};
178
179 fn package(id: &str, dependencies: &[&str]) -> PackageSnapshot {
180 PackageSnapshot {
181 id: PackageId::new(id),
182 manifest_name: id.to_owned(),
183 version: Version::new(1, 0, 0),
184 version_source: VersionSource::PackageManifest,
185 ecosystem: EcosystemId::RUST,
186 path: Utf8PathBuf::from(format!("crates/{id}")),
187 publishable: true,
188 dependencies: dependencies
189 .iter()
190 .map(|dependency| Dependency {
191 package: PackageId::new(*dependency),
192 kind: DependencyKind::Runtime,
193 requirement: None,
194 source: DependencySource::Manifest,
195 })
196 .collect(),
197 }
198 }
199
200 fn ids(order: Vec<PackageId>) -> Vec<String> {
201 order.into_iter().map(|id| id.to_string()).collect()
202 }
203
204 #[test]
205 fn orders_multi_level_dependencies() {
206 let graph = WorkspaceGraph::new(vec![
207 package("app", &["api"]),
208 package("api", &["core"]),
209 package("core", &[]),
210 ])
211 .unwrap();
212
213 assert_eq!(
214 ids(graph.topological_order().unwrap()),
215 ["core", "api", "app"]
216 );
217 }
218
219 #[test]
220 fn orders_diamond_dependencies_once() {
221 let graph = WorkspaceGraph::new(vec![
222 package("app", &["left", "right"]),
223 package("left", &["core"]),
224 package("right", &["core"]),
225 package("core", &[]),
226 ])
227 .unwrap();
228
229 assert_eq!(
230 ids(graph.topological_order().unwrap()),
231 ["core", "left", "right", "app"]
232 );
233 }
234
235 #[test]
236 fn orders_every_manifest_dependency_kind_before_the_dependent() {
237 let mut app = package(
238 "app",
239 &["runtime", "development", "build", "optional", "peer"],
240 );
241 for (dependency, kind) in app.dependencies.iter_mut().zip([
242 DependencyKind::Runtime,
243 DependencyKind::Development,
244 DependencyKind::Build,
245 DependencyKind::Optional,
246 DependencyKind::Peer,
247 ]) {
248 dependency.kind = kind;
249 }
250 let graph = WorkspaceGraph::new(vec![
251 app,
252 package("runtime", &[]),
253 package("development", &[]),
254 package("build", &[]),
255 package("optional", &[]),
256 package("peer", &[]),
257 ])
258 .unwrap();
259
260 assert_eq!(
261 ids(graph.topological_order().unwrap()),
262 ["build", "development", "optional", "peer", "runtime", "app"]
263 );
264 }
265
266 #[test]
267 fn orders_unrelated_packages_by_package_id() {
268 let graph =
269 WorkspaceGraph::new(vec![package("zebra", &[]), package("alpha", &[])]).unwrap();
270
271 assert_eq!(ids(graph.topological_order().unwrap()), ["alpha", "zebra"]);
272 }
273
274 #[test]
275 fn preserves_dynamic_ecosystem_identity_in_the_workspace_graph() {
276 let ecosystem = EcosystemId::new("com.example.engine").unwrap();
277 let mut plugin_package = package("game", &[]);
278 plugin_package.ecosystem = ecosystem.clone();
279
280 let graph = WorkspaceGraph::new(vec![plugin_package]).unwrap();
281
282 assert_eq!(
283 graph.package(&PackageId::new("game")).unwrap().ecosystem,
284 ecosystem
285 );
286 }
287
288 #[test]
289 fn reports_complete_dependency_cycle() {
290 let graph = WorkspaceGraph::new(vec![
291 package("a", &["b"]),
292 package("b", &["c"]),
293 package("c", &["a"]),
294 ])
295 .unwrap();
296
297 assert_eq!(
298 graph.topological_order(),
299 Err(WorkspaceGraphError::DependencyCycle {
300 cycle: vec![
301 PackageId::new("a"),
302 PackageId::new("b"),
303 PackageId::new("c"),
304 PackageId::new("a"),
305 ],
306 })
307 );
308 assert_eq!(
309 graph.topological_order().unwrap_err().to_string(),
310 "dependency cycle: a -> b -> c -> a"
311 );
312 }
313
314 #[test]
315 fn rejects_duplicate_package_ids() {
316 assert_eq!(
317 WorkspaceGraph::new(vec![package("core", &[]), package("core", &[])]),
318 Err(WorkspaceGraphError::DuplicatePackageId {
319 package: PackageId::new("core"),
320 })
321 );
322 }
323
324 #[test]
325 fn rejects_unknown_internal_dependencies() {
326 assert_eq!(
327 WorkspaceGraph::new(vec![package("app", &["missing"])]),
328 Err(WorkspaceGraphError::UnknownDependency {
329 package: PackageId::new("app"),
330 dependency: PackageId::new("missing"),
331 })
332 );
333 }
334}