use std::collections::{BTreeSet, HashMap, VecDeque};
use cargo_metadata::{DependencyKind, Metadata, Package, PackageId, TargetKind};
use petgraph::{Directed, graph::NodeIndex, visit::EdgeRef as _};
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub struct DepKindSet {
pub kind: BTreeSet<DepKind>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum DepKind {
Unknown,
Normal,
Build,
Dev,
ProcMacro,
}
impl std::fmt::Display for DepKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unknown => write!(f, "⚠️ unknown dependency type"),
Self::Normal => write!(f, "normal dependency"),
Self::Build => write!(f, "build-dependency"),
Self::Dev => write!(f, "dev-dependency"),
Self::ProcMacro => write!(f, "proc-macro"),
}
}
}
pub fn has_build_rs(package: &Package) -> bool {
package
.targets
.iter()
.any(|target| target.is_custom_build())
}
#[derive(Debug, Clone)]
struct Node {
id: PackageId,
kind: BTreeSet<DepKind>,
}
#[derive(Debug, Clone)]
struct Edge {
kind: BTreeSet<DepKind>,
}
impl From<DepKind> for Edge {
fn from(kind: DepKind) -> Self {
Self {
kind: std::iter::once(kind).collect(),
}
}
}
#[derive(Default)]
struct DepGraph {
graph: petgraph::Graph<Node, Edge, Directed>,
package_to_node: HashMap<PackageId, NodeIndex>,
}
impl DepGraph {
pub fn insert_node(&mut self, package_id: &PackageId, kind: DepKind) {
let node = Node {
id: package_id.clone(),
kind: std::iter::once(kind).collect(),
};
let node_idx = self.graph.add_node(node);
let prev = self.package_to_node.insert(package_id.clone(), node_idx);
assert!(prev.is_none(), "Node already existed: {package_id}");
}
pub fn node_of(&mut self, package_id: PackageId) -> NodeIndex {
match self.package_to_node.entry(package_id.clone()) {
std::collections::hash_map::Entry::Occupied(occupied_entry) => *occupied_entry.get(),
std::collections::hash_map::Entry::Vacant(vacant_entry) => {
let node = Node {
id: package_id,
kind: BTreeSet::new(),
};
let node_idx = self.graph.add_node(node);
vacant_entry.insert(node_idx);
node_idx
}
}
}
pub fn add_edge(&mut self, dependent_: PackageId, dependency: PackageId, edge: Edge) {
let dependent = self.node_of(dependent_);
let dependency = self.node_of(dependency);
self.graph.add_edge(dependent, dependency, edge);
}
pub fn from_metadata(
metadata: &Metadata,
starting_packages: &[PackageId],
) -> anyhow::Result<Self> {
use std::collections::VecDeque;
let mut graph = Self::default();
let mut queue = VecDeque::new();
let mut visited = std::collections::HashSet::new();
for package_id in starting_packages {
if visited.insert(package_id.clone()) {
queue.push_back(package_id.clone());
}
graph.insert_node(package_id, DepKind::Normal);
}
while let Some(package_id) = queue.pop_front() {
let package = metadata
.packages
.iter()
.find(|p| p.id == package_id)
.ok_or_else(|| anyhow::anyhow!("Package not found: {:?}", package_id))?;
for dep in &package.dependencies {
if let Some(dep_package) = metadata
.packages
.iter()
.find(|p| p.name.as_str() == dep.name.as_str())
{
let mut edge_kind = BTreeSet::new();
if dep_package
.targets
.iter()
.all(|t| t.kind.iter().all(|k| k == &TargetKind::ProcMacro))
{
edge_kind.insert(DepKind::ProcMacro);
} else {
edge_kind.insert(match dep.kind {
DependencyKind::Normal => DepKind::Normal,
DependencyKind::Build => DepKind::Build,
DependencyKind::Development => DepKind::Dev,
DependencyKind::Unknown => DepKind::Unknown,
});
}
if edge_kind.is_empty() {
println!("WARNING: dependency edge has no kind");
}
let edge = Edge { kind: edge_kind };
graph.add_edge(package_id.clone(), dep_package.id.clone(), edge);
if visited.insert(dep_package.id.clone()) {
queue.push_back(dep_package.id.clone());
}
}
}
}
Ok(graph)
}
fn analyze(mut self) -> HashMap<PackageId, DepKindSet> {
self.compute_dependency_kinds();
self.package_to_node
.iter()
.map(|(package_id, &node_idx)| {
let node = &self.graph[node_idx];
let set = DepKindSet {
kind: node.kind.clone(),
};
(package_id.clone(), set)
})
.collect()
}
fn compute_dependency_kinds(&mut self) {
let mut queue = VecDeque::new();
#[expect(clippy::iter_over_hash_type)]
for &node_idx in self.package_to_node.values() {
if !self.graph[node_idx].kind.is_empty() {
queue.push_back(node_idx);
}
}
while let Some(node_idx) = queue.pop_front() {
let node_kind = self.graph[node_idx].kind.clone();
let node_edges: Vec<(NodeIndex, Edge)> = self
.graph
.edges(node_idx)
.map(|edge| (edge.target(), edge.weight().clone()))
.collect();
for (dependency_idx, edge_data) in node_edges {
let new_kinds = dependency_kind_from_edge_and_dependent(
&edge_data,
&Node {
id: self.graph[node_idx].id.clone(),
kind: node_kind.clone(),
},
);
let current_kinds = &self.graph[dependency_idx].kind;
let missing_kinds: BTreeSet<_> =
new_kinds.difference(current_kinds).copied().collect();
if !missing_kinds.is_empty() {
self.graph[dependency_idx].kind.extend(missing_kinds);
queue.push_back(dependency_idx);
}
}
}
}
}
pub fn analyze_dependency_graph(
metadata: &Metadata,
sinks: &[PackageId],
) -> anyhow::Result<HashMap<PackageId, DepKindSet>> {
Ok(DepGraph::from_metadata(metadata, sinks)?.analyze())
}
fn dependency_kind_from_edge_and_dependent(edge: &Edge, dependent: &Node) -> BTreeSet<DepKind> {
let mut final_set = BTreeSet::default();
for &dep_kind in &edge.kind {
for &crate_kind in &dependent.kind {
#[expect(clippy::match_same_arms)]
let new_kind = match (dep_kind, crate_kind) {
(_, DepKind::ProcMacro) => DepKind::ProcMacro,
(DepKind::Normal, crate_kind) => crate_kind,
(DepKind::Build, _) => DepKind::Build,
(DepKind::Dev, _) => DepKind::Dev,
(DepKind::ProcMacro, _) => DepKind::ProcMacro,
(_, DepKind::Unknown) | (DepKind::Unknown, _) => DepKind::Unknown,
};
final_set.insert(new_kind);
}
}
final_set
}
#[cfg(test)]
mod tests {
#![allow(clippy::single_char_pattern)]
use cargo_metadata::PackageId;
use super::*;
fn pid(s: &str) -> PackageId {
PackageId { repr: s.to_owned() }
}
fn set(crate_kind: DepKind) -> DepKindSet {
DepKindSet {
kind: std::iter::once(crate_kind).collect(),
}
}
#[test]
fn test_graph() {
let mut graph = DepGraph::default();
graph.insert_node(&pid("binary"), DepKind::Normal);
graph.add_edge(pid("binary"), pid("build_dep"), Edge::from(DepKind::Build));
graph.add_edge(pid("build_dep"), pid("3rd"), Edge::from(DepKind::Normal));
let result = graph.analyze();
assert_eq!(&result[&pid("binary")], &set(DepKind::Normal));
assert_eq!(&result[&pid("build_dep")], &set(DepKind::Build));
assert_eq!(&result[&pid("3rd")], &set(DepKind::Build));
}
#[test]
fn test_proc_macro() {
let mut graph = DepGraph::default();
graph.insert_node(&pid("binary"), DepKind::Normal);
graph.insert_node(&pid("clap_derive"), DepKind::ProcMacro);
graph.add_edge(
pid("binary"),
pid("clap_derive"),
Edge::from(DepKind::Normal),
);
graph.add_edge(
pid("clap_derive"),
pid("proc-macro2"),
Edge::from(DepKind::Normal),
);
let result = graph.analyze();
}
#[test]
fn test_proc_macro2() {
let mut graph = DepGraph::default();
graph.insert_node(&pid("binary"), DepKind::Normal);
graph.add_edge(
pid("binary"),
pid("clap_derive"),
Edge::from(DepKind::ProcMacro),
);
graph.add_edge(
pid("clap_derive"),
pid("proc-macro2"),
Edge::from(DepKind::Normal),
);
let result = graph.analyze();
assert_eq!(&result[&pid("binary")], &set(DepKind::Normal));
assert_eq!(&result[&pid("clap_derive")], &set(DepKind::ProcMacro));
assert_eq!(&result[&pid("proc-macro2")], &set(DepKind::ProcMacro));
}
}