use anyhow::{anyhow, Context, Result};
use cargo_metadata::MetadataCommand;
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::dependency_graph::DependencyGraph;
use crate::scanner::{CodeScanner, UsageLocation};
#[derive(Debug, Serialize, Deserialize)]
pub struct AnalysisResult {
pub dependency_name: String,
pub found: bool,
pub versions: Vec<VersionInfo>,
pub total_usage_count: usize,
pub usage_locations: Option<Vec<UsageLocation>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VersionInfo {
pub version: String,
pub used_by: Vec<DependentInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DependentInfo {
pub name: String,
pub version: String,
pub is_workspace_member: bool,
}
impl AnalysisResult {
pub fn print(&self) {
if !self.found {
println!(
"{}",
format!(
"Dependency '{}' not found in the dependency tree",
self.dependency_name
)
.red()
);
return;
}
println!(
"\n{} {}",
"Analyzing dependency:".bold(),
self.dependency_name.cyan().bold()
);
for version_info in &self.versions {
println!(
"\n {} {}",
"Version:".bold(),
version_info.version.yellow()
);
if version_info.used_by.is_empty() {
println!(" {}", "No direct dependents found".dimmed());
} else {
println!(" {}:", "Used by".bold());
for dep in &version_info.used_by {
let marker = if dep.is_workspace_member {
"[workspace]".green()
} else {
"[external]".blue()
};
println!(
" {} {} {} {}",
"•".cyan(),
dep.name.white(),
dep.version.dimmed(),
marker
);
}
}
}
if self.total_usage_count > 0 {
println!(
"\n {} {} {}",
"Total imports found:".bold(),
self.total_usage_count.to_string().green().bold(),
"in codebase".dimmed()
);
}
if let Some(locations) = &self.usage_locations {
println!("\n {}:", "Usage locations".bold());
let mut sorted_locations = locations.clone();
sorted_locations.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
for loc in sorted_locations {
println!(
" {} {}:{}",
"•".cyan(),
loc.file.white(),
loc.line.to_string().yellow()
);
println!(" {}", loc.content.dimmed());
}
} else if self.total_usage_count > 0 {
println!(
"\n {} Run with {} to see where it's used",
"Tip:".bold().yellow(),
"--where-used".cyan()
);
}
println!();
}
}
pub struct DependencyAnalyzer {
graph: DependencyGraph,
project_path: PathBuf,
}
impl DependencyAnalyzer {
pub fn new(manifest_path: PathBuf) -> Result<Self> {
let metadata = MetadataCommand::new()
.manifest_path(manifest_path.join("Cargo.toml"))
.exec()
.context("Failed to execute cargo metadata")?;
let graph =
DependencyGraph::from_metadata(metadata).context("Failed to build dependency graph")?;
Ok(Self {
graph,
project_path: manifest_path,
})
}
pub fn analyze(
&self,
dependency_name: &str,
scan_usage: bool,
all_versions: bool,
) -> Result<AnalysisResult> {
let packages = self.graph.find_packages_by_name(dependency_name);
if packages.is_empty() {
return Ok(AnalysisResult {
dependency_name: dependency_name.to_string(),
found: false,
versions: Vec::new(),
total_usage_count: 0,
usage_locations: None,
});
}
let packages_to_analyze = if all_versions {
packages
} else {
vec![*packages
.first()
.ok_or_else(|| anyhow!("No packages found for dependency '{}'", dependency_name))?]
};
let mut versions = Vec::new();
for package in packages_to_analyze {
let dependents = self.graph.find_reverse_dependencies(&package.id);
let mut dependent_infos: Vec<DependentInfo> = dependents
.into_iter()
.map(|dep| DependentInfo {
name: dep.name.clone(),
version: dep.version.to_string(),
is_workspace_member: self.graph.is_workspace_member(&dep.id),
})
.collect();
dependent_infos.sort_by(|a, b| {
b.is_workspace_member
.cmp(&a.is_workspace_member)
.then(a.name.cmp(&b.name))
});
versions.push(VersionInfo {
version: package.version.to_string(),
used_by: dependent_infos,
});
}
let (total_usage_count, usage_locations) = if scan_usage {
let locations = CodeScanner::scan_for_usage(&self.project_path, dependency_name)
.context("Failed to scan codebase for usage")?;
let count = locations.len();
(count, Some(locations))
} else {
let locations = CodeScanner::scan_for_usage(&self.project_path, dependency_name)
.unwrap_or_default();
(locations.len(), None)
};
Ok(AnalysisResult {
dependency_name: dependency_name.to_string(),
found: true,
versions,
total_usage_count,
usage_locations,
})
}
}