Skip to main content

check_deprule/dependency_graph/
mod.rs

1use 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            // https://github.com/rust-lang/cargo/issues/7752
65            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    // prune nodes not reachable from the root package (directionally)
84    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}