use crate::error::Result;
use cargo_metadata::{DependencyKind as CargoDependencyKind, MetadataCommand, Package};
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::HashMap;
use std::path::Path;
pub struct DependencyAnalyzer {
metadata: cargo_metadata::Metadata,
graph: DiGraph<String, ()>,
node_map: HashMap<String, NodeIndex>,
}
#[derive(Debug, Clone)]
pub struct CrateInfo {
pub name: String,
pub version: String,
pub authors: Vec<String>,
pub license: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub repository: Option<String>,
pub documentation: Option<String>,
pub dependencies: Vec<DependencyInfo>,
pub features: Vec<String>,
pub default_features: Vec<String>,
pub edition: String,
pub rust_version: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DependencyInfo {
pub name: String,
pub version: String,
pub optional: bool,
pub features: Vec<String>,
pub kind: DependencyKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyKind {
Normal,
Dev,
Build,
}
impl std::fmt::Display for DependencyKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DependencyKind::Normal => write!(f, "normal"),
DependencyKind::Dev => write!(f, "dev"),
DependencyKind::Build => write!(f, "build"),
}
}
}
impl DependencyAnalyzer {
pub fn from_manifest(manifest_path: &Path) -> Result<Self> {
let metadata = MetadataCommand::new().manifest_path(manifest_path).exec()?;
Ok(Self::from_metadata(metadata))
}
pub fn from_current_dir() -> Result<Self> {
let metadata = MetadataCommand::new().exec()?;
Ok(Self::from_metadata(metadata))
}
fn from_metadata(metadata: cargo_metadata::Metadata) -> Self {
let mut graph = DiGraph::new();
let mut node_map = HashMap::new();
for package in &metadata.packages {
let node = graph.add_node(package.name.clone());
node_map.insert(package.name.clone(), node);
}
for package in &metadata.packages {
if let Some(&from_node) = node_map.get(&package.name) {
for dep in &package.dependencies {
if let Some(&to_node) = node_map.get(&dep.name) {
graph.add_edge(from_node, to_node, ());
}
}
}
}
Self {
metadata,
graph,
node_map,
}
}
pub fn root_package(&self) -> Option<CrateInfo> {
self.metadata
.root_package()
.map(|pkg| self.package_to_info(pkg))
}
pub fn get_crate_info(&self, name: &str) -> Option<CrateInfo> {
self.metadata
.packages
.iter()
.find(|p| p.name == name)
.map(|pkg| self.package_to_info(pkg))
}
pub fn all_packages(&self) -> Vec<CrateInfo> {
self.metadata
.packages
.iter()
.map(|pkg| self.package_to_info(pkg))
.collect()
}
pub fn direct_dependencies(&self, name: &str) -> Vec<DependencyInfo> {
self.metadata
.packages
.iter()
.find(|p| p.name == name)
.map(|pkg| self.extract_dependencies(pkg))
.unwrap_or_default()
}
pub fn dependency_tree(&self, root: &str) -> Vec<(String, usize)> {
if let Some(&root_node) = self.node_map.get(root) {
let mut result = Vec::new();
let mut visited = HashMap::new();
self.traverse_deps(root_node, 0, &mut result, &mut visited);
result
} else {
Vec::new()
}
}
pub fn total_dependency_count(&self, name: &str) -> usize {
self.dependency_tree(name).len().saturating_sub(1)
}
fn traverse_deps(
&self,
node: NodeIndex,
depth: usize,
result: &mut Vec<(String, usize)>,
visited: &mut HashMap<NodeIndex, bool>,
) {
if visited.contains_key(&node) {
return;
}
visited.insert(node, true);
let name = self.graph[node].clone();
result.push((name, depth));
for neighbor in self.graph.neighbors(node) {
self.traverse_deps(neighbor, depth + 1, result, visited);
}
}
fn package_to_info(&self, pkg: &Package) -> CrateInfo {
let dependencies = self.extract_dependencies(pkg);
let features: Vec<String> = pkg.features.keys().cloned().collect();
let default_features = pkg.features.get("default").cloned().unwrap_or_default();
CrateInfo {
name: pkg.name.clone(),
version: pkg.version.to_string(),
authors: pkg.authors.clone(),
license: pkg.license.clone(),
description: pkg.description.clone(),
homepage: pkg.homepage.clone(),
repository: pkg.repository.clone(),
documentation: pkg.documentation.clone(),
dependencies,
features,
default_features,
edition: pkg.edition.to_string(),
rust_version: pkg.rust_version.as_ref().map(|v| v.to_string()),
}
}
fn extract_dependencies(&self, pkg: &Package) -> Vec<DependencyInfo> {
pkg.dependencies
.iter()
.map(|dep| DependencyInfo {
name: dep.name.clone(),
version: dep.req.to_string(),
optional: dep.optional,
features: dep.features.clone(),
kind: match dep.kind {
CargoDependencyKind::Normal => DependencyKind::Normal,
CargoDependencyKind::Development => DependencyKind::Dev,
CargoDependencyKind::Build => DependencyKind::Build,
_ => DependencyKind::Normal,
},
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_dependency_tree_from_manifest() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
if !manifest.exists() {
return;
}
let analyzer = DependencyAnalyzer::from_manifest(&manifest).unwrap();
let root = analyzer.root_package().expect("root package");
assert_eq!(root.name, "rustlens");
let tree = analyzer.dependency_tree(&root.name);
assert!(!tree.is_empty());
assert_eq!(tree[0].0, root.name);
assert_eq!(tree[0].1, 0);
}
#[test]
fn test_direct_dependencies() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
if !manifest.exists() {
return;
}
let analyzer = DependencyAnalyzer::from_manifest(&manifest).unwrap();
let root = analyzer.root_package().unwrap();
let deps = analyzer.direct_dependencies(&root.name);
assert!(deps
.iter()
.any(|d| d.name == "ratatui" || d.name == "crossterm"));
}
}