use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context, Result};
use regex::Regex;
use walkdir::WalkDir;
use crate::models::CrateReference;
use crate::utils::is_std_crate;
pub struct DependencyAnalyzer {
project_root: PathBuf,
debug: bool,
}
impl DependencyAnalyzer {
pub fn new(project_root: PathBuf) -> Self {
Self {
project_root,
debug: false,
}
}
pub fn with_debug(project_root: PathBuf, debug: bool) -> Self {
Self {
project_root,
debug,
}
}
pub fn analyze_dependencies(&self) -> Result<HashMap<String, CrateReference>> {
let mut crate_refs = HashMap::new();
let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
let nested_regex = Regex::new(r"\{([^}]*)\}")?;
let item_regex = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)")?;
let output = Command::new("rust-analyzer")
.arg("analysis")
.arg("--workspace")
.current_dir(&self.project_root)
.output()
.context("Failed to run rust-analyzer. Is it installed?")?;
if !output.status.success() {
println!("Warning: rust-analyzer analysis returned non-zero status. Falling back to regex-based analysis.");
}
for entry in WalkDir::new(&self.project_root) {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "rs") {
if self.debug {
println!("Found Rust file: {:?}", path);
}
let content = fs::read_to_string(path)?;
let file_path = path.to_path_buf();
self.analyze_file(FileAnalysisContext {
content: &content,
file_path: &file_path,
use_regex: &use_regex,
extern_regex: &extern_regex,
nested_regex: &nested_regex,
item_regex: &item_regex,
crate_refs: &mut crate_refs,
})?;
}
}
if self.debug {
println!("\nFinal crate references:");
for (name, crate_ref) in &crate_refs {
println!("- {} (used in {} files)", name, crate_ref.usage_count());
println!(" Used in:");
for path in &crate_ref.used_in {
println!(" - {:?}", path);
}
}
}
Ok(crate_refs)
}
fn analyze_file(&self, ctx: FileAnalysisContext) -> Result<()> {
if self.debug {
println!("Analyzing file: {:?}", ctx.file_path);
}
let FileAnalysisContext {
content,
file_path,
use_regex,
extern_regex,
nested_regex,
item_regex,
crate_refs,
} = ctx;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") {
continue;
}
if self.debug {
println!("Processing line: {}", line);
}
if let Some(cap) = use_regex.captures(line) {
let full_path = cap[1].to_string();
let base_crate = full_path
.split("::")
.next()
.unwrap_or(&full_path)
.to_string();
if self.debug {
println!(
"Found use statement: {} -> base crate: {}",
full_path, base_crate
);
}
if !is_std_crate(&base_crate) &&
!base_crate.starts_with("std::") &&
!base_crate.starts_with("core::") &&
!base_crate.starts_with("alloc::") {
let crate_ref = crate_refs
.entry(base_crate.clone())
.or_insert_with(|| CrateReference::new(base_crate.clone()));
crate_ref.add_usage(file_path.clone());
if self.debug {
println!("Added crate reference: {}", crate_ref.name);
}
}
if let Some(nested) = nested_regex.captures(line) {
if let Some(items) = nested.get(1) {
for item in item_regex.captures_iter(items.as_str()) {
let item_name = item[1].to_string();
if self.debug {
println!("Processing nested item: {}", item_name);
}
if !is_std_crate(&item_name) &&
!item_name.starts_with("std::") {
let crate_ref = crate_refs
.entry(item_name.clone())
.or_insert_with(|| CrateReference::new(item_name.clone()));
crate_ref.add_usage(file_path.clone());
if self.debug {
println!("Added nested crate reference: {}", crate_ref.name);
}
}
}
}
}
}
if let Some(cap) = extern_regex.captures(line) {
let crate_name = cap[1].to_string();
if self.debug {
println!("Found extern crate: {}", crate_name);
}
if !is_std_crate(&crate_name) {
let crate_ref = crate_refs
.entry(crate_name.clone())
.or_insert_with(|| CrateReference::new(crate_name.clone()));
crate_ref.add_usage(file_path.clone());
if self.debug {
println!("Added extern crate reference: {}", crate_ref.name);
}
}
}
}
if self.debug {
println!("Current crate references:");
for (name, crate_ref) in crate_refs {
println!("- {} (used in {} files)", name, crate_ref.usage_count());
}
}
Ok(())
}
}
struct FileAnalysisContext<'a> {
content: &'a str,
file_path: &'a PathBuf,
use_regex: &'a Regex,
extern_regex: &'a Regex,
nested_regex: &'a Regex,
item_regex: &'a Regex,
crate_refs: &'a mut HashMap<String, CrateReference>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
fn create_test_file(dir: &TempDir, name: &str, content: &str) -> Result<PathBuf> {
let path = dir.path().join(name);
let mut file = File::create(&path)?;
writeln!(file, "{}", content.trim())?;
Ok(path)
}
#[test]
fn test_analyze_dependencies() -> Result<()> {
let temp_dir = TempDir::new()?;
let main_rs = create_test_file(
&temp_dir,
"main.rs",
r#"use serde::Serialize;
use tokio::runtime::Runtime;
use anyhow::Result;
use std::fs;"#,
)?;
let lib_rs = create_test_file(
&temp_dir,
"lib.rs",
r#"use serde::{Deserialize, Serialize};
use regex::Regex;
extern crate serde;"#,
)?;
println!("\nTest files created:");
println!("main.rs content:\n{}", fs::read_to_string(&main_rs)?);
println!("lib.rs content:\n{}", fs::read_to_string(&lib_rs)?);
println!("\nStarting analysis...\n");
let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
let crate_refs = analyzer.analyze_dependencies()?;
println!("\nAnalysis complete. Found crates:");
for (name, crate_ref) in &crate_refs {
println!("- {} (used in {} files)", name, crate_ref.usage_count());
println!(" Used in:");
for path in &crate_ref.used_in {
if let Ok(relative) = path.strip_prefix(temp_dir.path()) {
println!(" - {}", relative.display());
}
}
}
assert!(
crate_refs.contains_key("serde"),
"serde dependency not found"
);
assert!(
crate_refs.contains_key("tokio"),
"tokio dependency not found"
);
assert!(
crate_refs.contains_key("anyhow"),
"anyhow dependency not found"
);
assert!(
crate_refs.contains_key("regex"),
"regex dependency not found"
);
let serde_ref = crate_refs.get("serde").unwrap();
assert_eq!(
serde_ref.usage_count(),
2,
"serde should be used in two files"
);
Ok(())
}
#[test]
fn test_analyze_file() -> Result<()> {
let temp_dir = TempDir::new()?;
let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
let file_path = temp_dir.path().join("test.rs");
let content = r#"use serde::Serialize;
use tokio::runtime::Runtime;
extern crate anyhow;
use std::fs;"#;
println!("\nTest file content:\n{}", content);
println!("\nStarting analysis...\n");
let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
let nested_regex = Regex::new(r"\{([^}]*)\}")?;
let item_regex = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)")?;
let mut crate_refs = HashMap::new();
analyzer.analyze_file(FileAnalysisContext {
content,
file_path: &file_path,
use_regex: &use_regex,
extern_regex: &extern_regex,
nested_regex: &nested_regex,
item_regex: &item_regex,
crate_refs: &mut crate_refs,
})?;
println!("\nAnalysis complete. Found crates:");
for (name, crate_ref) in &crate_refs {
println!("- {} (used in {} files)", name, crate_ref.usage_count());
}
assert!(
crate_refs.contains_key("serde"),
"serde dependency not found"
);
assert!(
crate_refs.contains_key("tokio"),
"tokio dependency not found"
);
assert!(
crate_refs.contains_key("anyhow"),
"anyhow dependency not found"
);
Ok(())
}
}