cargo-whys 0.1.0

A cargo subcommand that explains why dependencies are in your tree
Documentation
use anyhow::Result;
use cargo_metadata::{Metadata, Package, PackageId};
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::HashMap;

pub struct DependencyGraph {
    graph: DiGraph<PackageId, ()>,
    node_map: HashMap<PackageId, NodeIndex>,
    metadata: Metadata,
}

impl DependencyGraph {
    pub fn from_metadata(metadata: Metadata) -> Result<Self> {
        let mut graph = DiGraph::new();
        let mut node_map = HashMap::new();

        for package in &metadata.packages {
            let node = graph.add_node(package.id.clone());
            node_map.insert(package.id.clone(), node);
        }

        for package in &metadata.packages {
            let source_node = node_map[&package.id];
            for dep in &package.dependencies {
                if let Some(resolved) = metadata
                    .packages
                    .iter()
                    .find(|p| p.name == dep.name && dep.req.matches(&p.version))
                {
                    if let Some(&target_node) = node_map.get(&resolved.id) {
                        graph.add_edge(source_node, target_node, ());
                    }
                }
            }
        }

        Ok(Self {
            graph,
            node_map,
            metadata,
        })
    }

    pub fn find_packages_by_name(&self, name: &str) -> Vec<&Package> {
        self.metadata
            .packages
            .iter()
            .filter(|p| p.name == name)
            .collect()
    }

    pub fn find_reverse_dependencies(&self, package_id: &PackageId) -> Vec<&Package> {
        let mut result = Vec::new();

        if let Some(&target_node) = self.node_map.get(package_id) {
            let incoming = self
                .graph
                .neighbors_directed(target_node, petgraph::Direction::Incoming);

            for node in incoming {
                let pkg_id = &self.graph[node];
                if let Some(pkg) = self.metadata.packages.iter().find(|p| &p.id == pkg_id) {
                    result.push(pkg);
                }
            }
        }

        result
    }

    pub fn is_workspace_member(&self, package_id: &PackageId) -> bool {
        self.metadata
            .workspace_members
            .iter()
            .any(|id| id == package_id)
    }
}