check_deprule/dependency_graph/
mod.rs1use anyhow::{Error, anyhow};
2use cargo_metadata::{DependencyKind, Metadata, Package, PackageId};
3use petgraph::graph::NodeIndex;
4use petgraph::stable_graph::StableGraph;
5use petgraph::visit::Dfs;
6use std::collections::HashMap;
7
8pub(crate) mod formatter;
9pub mod tree;
10pub mod violation;
11
12#[derive(Debug, Clone)]
13pub struct Graph {
14 pub graph: StableGraph<Package, DependencyKind>,
15 pub nodes: HashMap<PackageId, NodeIndex>,
16 pub root: Option<PackageId>,
17}
18
19#[derive(Debug, Clone, Default)]
20pub struct DependencyGraphBuildConfigs {
21 no_dev_dependencies: bool,
22}
23impl DependencyGraphBuildConfigs {
24 pub fn new(no_dev_dependencies: bool) -> Self {
25 Self {
26 no_dev_dependencies,
27 }
28 }
29}
30
31#[tracing::instrument(skip(metadata), fields(packages = metadata.packages.len()))]
32pub fn build_dependency_graph(
33 metadata: &Metadata,
34 config: DependencyGraphBuildConfigs,
35) -> Result<Graph, Error> {
36 let resolve = metadata
37 .resolve
38 .as_ref()
39 .ok_or_else(|| anyhow!("cargo metadata did not return dependency resolve information"))?;
40
41 let mut graph = Graph {
42 graph: StableGraph::new(),
43 nodes: HashMap::new(),
44 root: resolve.root.clone(),
45 };
46
47 for package in &metadata.packages {
48 let id = package.id.clone();
49 let index = graph.graph.add_node(package.clone());
50 graph.nodes.insert(id, index);
51 }
52
53 for node in &resolve.nodes {
54 if node.deps.len() != node.dependencies.len() {
55 return Err(anyhow!("cargo tree requires cargo 1.41 or newer"));
56 }
57
58 let from = graph.nodes[&node.id];
59 for dep in &node.deps {
60 if dep.dep_kinds.is_empty() {
61 return Err(anyhow!("cargo tree requires cargo 1.41 or newer"));
62 }
63
64 let mut kinds = vec![];
66 for kind in &dep.dep_kinds {
67 if !kinds.contains(&kind.kind) {
68 kinds.push(kind.kind);
69 }
70 }
71
72 let to = graph.nodes[&dep.pkg];
73 for kind in kinds {
74 if config.no_dev_dependencies && kind == DependencyKind::Development {
75 continue;
76 }
77
78 graph.graph.add_edge(from, to, kind);
79 }
80 }
81 }
82
83 if let Some(root) = &graph.root {
85 let mut dfs = Dfs::new(&graph.graph, graph.nodes[root]);
86 while dfs.next(&graph.graph).is_some() {}
87
88 let g = &mut graph.graph;
89 graph.nodes.retain(|_, idx| {
90 if !dfs.discovered.contains(idx.index()) {
91 g.remove_node(*idx);
92 false
93 } else {
94 true
95 }
96 });
97 }
98
99 Ok(graph)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::metadata::{CollectMetadataConfig, collect_metadata};
106 use anyhow::Result;
107
108 fn clean_arch_metadata() -> Result<Metadata> {
109 collect_metadata(CollectMetadataConfig {
110 manifest_path: Some("tests/demo_crates/clean-arch/Cargo.toml".to_string()),
111 ..CollectMetadataConfig::default()
112 })
113 }
114
115 #[test]
116 fn test_build_dependency_graph_creates_nodes() -> Result<()> {
117 let metadata = clean_arch_metadata()?;
118 let workspace_count = metadata.workspace_members.len();
119 let graph = build_dependency_graph(&metadata, DependencyGraphBuildConfigs::default())?;
120
121 assert!(graph.nodes.len() >= workspace_count);
122 Ok(())
123 }
124
125 #[test]
126 fn test_build_dependency_graph_workspace_members_present() -> Result<()> {
127 let metadata = clean_arch_metadata()?;
128 let graph = build_dependency_graph(&metadata, DependencyGraphBuildConfigs::default())?;
129
130 for id in &metadata.workspace_members {
131 assert!(
132 graph.nodes.contains_key(id),
133 "missing workspace member: {id}"
134 );
135 }
136 Ok(())
137 }
138
139 #[test]
140 fn test_build_dependency_graph_no_dev_dependencies() -> Result<()> {
141 let metadata = clean_arch_metadata()?;
142 let config_with_dev = DependencyGraphBuildConfigs::new(false);
143 let graph_with_dev = build_dependency_graph(&metadata, config_with_dev)?;
144
145 let config_no_dev = DependencyGraphBuildConfigs::new(true);
146 let graph_no_dev = build_dependency_graph(&metadata, config_no_dev)?;
147
148 assert!(
149 graph_no_dev.graph.edge_count() <= graph_with_dev.graph.edge_count(),
150 "no_dev_dependencies should result in equal or fewer edges"
151 );
152 Ok(())
153 }
154
155 #[test]
156 fn test_dependency_graph_build_configs_default() {
157 let config = DependencyGraphBuildConfigs::default();
158 assert!(!config.no_dev_dependencies);
159 }
160
161 #[test]
162 fn test_dependency_graph_build_configs_new() {
163 let config = DependencyGraphBuildConfigs::new(true);
164 assert!(config.no_dev_dependencies);
165
166 let config = DependencyGraphBuildConfigs::new(false);
167 assert!(!config.no_dev_dependencies);
168 }
169}