use crate::types::{CallSite, ProjectAnalysis};
use anyhow::{Context, Result};
use cargo_metadata::{MetadataCommand, PackageId};
use std::collections::HashSet;
use std::path::Path;
use syn::visit::{self, Visit};
use walkdir::WalkDir;
pub struct DependencyAnalyzer {
metadata: cargo_metadata::Metadata,
direct_deps: HashSet<String>,
all_deps: HashSet<String>,
}
impl DependencyAnalyzer {
pub fn new() -> Result<Self> {
let metadata = MetadataCommand::new()
.exec()
.context("Failed to execute cargo metadata")?;
let resolve = metadata
.resolve
.as_ref()
.context("No resolve information in metadata")?;
let root_id = resolve
.root
.as_ref()
.context("No root package in resolve")?;
let direct_deps = Self::get_direct_dependency_names(&metadata, root_id)?;
let all_deps = Self::get_all_dependency_names(&metadata, root_id)?;
Ok(Self {
metadata,
direct_deps,
all_deps,
})
}
pub fn analyze(&self) -> Result<ProjectAnalysis> {
let resolve = self
.metadata
.resolve
.as_ref()
.context("No resolve information")?;
let root_id = resolve.root.as_ref().context("No root package")?;
let root_pkg = self
.metadata
.packages
.iter()
.find(|p| &p.id == root_id)
.context("Root package not found")?;
let mut analysis = ProjectAnalysis::new(root_pkg.name.clone());
if !Path::new("src").exists() {
return Ok(analysis);
}
for entry in WalkDir::new("src")
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "rs"))
{
let file_path = entry.path();
self.analyze_file(file_path, &mut analysis)?;
}
analysis.recalculate_totals();
Ok(analysis)
}
fn analyze_file(&self, file_path: &Path, analysis: &mut ProjectAnalysis) -> Result<()> {
let content = std::fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {:?}", file_path))?;
let syntax_tree = syn::parse_file(&content).with_context(|| {
format!("Failed to parse Rust file: {:?}", file_path)
})?;
let mut visitor = CallVisitor {
file_path: file_path.to_string_lossy().to_string(),
direct_deps: &self.direct_deps,
all_deps: &self.all_deps,
analysis,
};
visitor.visit_file(&syntax_tree);
Ok(())
}
fn get_direct_dependency_names(
metadata: &cargo_metadata::Metadata,
root_id: &PackageId,
) -> Result<HashSet<String>> {
let root_pkg = metadata
.packages
.iter()
.find(|p| &p.id == root_id)
.context("Root package not found")?;
Ok(root_pkg
.dependencies
.iter()
.map(|d| d.name.clone())
.collect())
}
fn get_all_dependency_names(
metadata: &cargo_metadata::Metadata,
root_id: &PackageId,
) -> Result<HashSet<String>> {
let resolve = metadata
.resolve
.as_ref()
.context("No resolve information")?;
let all_dep_ids: HashSet<&PackageId> = resolve
.nodes
.iter()
.filter(|n| &n.id != root_id)
.map(|n| &n.id)
.collect();
Ok(metadata
.packages
.iter()
.filter(|p| all_dep_ids.contains(&p.id))
.map(|p| p.name.clone())
.collect())
}
}
struct CallVisitor<'a> {
file_path: String,
direct_deps: &'a HashSet<String>,
all_deps: &'a HashSet<String>,
analysis: &'a mut ProjectAnalysis,
}
impl<'a> CallVisitor<'a> {
fn extract_crate_and_method(&self, path: &syn::Path) -> Option<(String, String)> {
if path.segments.is_empty() {
return None;
}
let first_segment = path.segments.first()?.ident.to_string();
if !self.all_deps.contains(&first_segment) {
return None;
}
let full_path: Vec<String> = path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
Some((first_segment, full_path.join("::")))
}
fn record_call(&mut self, crate_name: String, method_path: String, line: usize, column: usize) {
let is_direct = self.direct_deps.contains(&crate_name);
let crate_usage = self.analysis.get_or_create_crate(crate_name, is_direct);
let call_site = CallSite {
file_path: self.file_path.clone(),
line,
column,
};
crate_usage.add_call(method_path, call_site);
}
}
impl<'a> Visit<'a> for CallVisitor<'a> {
fn visit_expr_call(&mut self, node: &'a syn::ExprCall) {
if let syn::Expr::Path(expr_path) = &*node.func {
if let Some((crate_name, method_path)) = self.extract_crate_and_method(&expr_path.path)
{
self.record_call(crate_name, method_path, 0, 0);
}
}
visit::visit_expr_call(self, node);
}
fn visit_expr_method_call(&mut self, node: &'a syn::ExprMethodCall) {
let method_name = node.method.to_string();
if let syn::Expr::Path(expr_path) = &*node.receiver {
if let Some((crate_name, base_path)) = self.extract_crate_and_method(&expr_path.path)
{
let full_method_path = format!("{}::{}", base_path, method_name);
self.record_call(crate_name, full_method_path, 0, 0);
}
}
visit::visit_expr_method_call(self, node);
}
fn visit_macro(&mut self, node: &'a syn::Macro) {
if let Some((crate_name, method_path)) = self.extract_crate_and_method(&node.path) {
self.record_call(crate_name, method_path, 0, 0);
}
visit::visit_macro(self, node);
}
fn visit_use_tree(&mut self, node: &'a syn::UseTree) {
visit::visit_use_tree(self, node);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyzer_creation() {
let result = DependencyAnalyzer::new();
assert!(result.is_ok() || result.is_err()); }
}