use rustc_hash::FxHashSet;
use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use cargo_metadata::{Package, TargetKind};
use cargo_util_schemas::core::PackageIdSpec;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use walkdir::{DirEntry, WalkDir};
use crate::import_collector::collect_imports;
use anyhow::{Result, anyhow};
pub type Dependencies = FxHashSet<String>;
pub struct DependencyAnalyzer {
expand_macros: bool,
}
impl DependencyAnalyzer {
pub const fn new(expand_macros: bool) -> Self {
Self { expand_macros }
}
pub fn analyze_package(&self, package: &Package) -> Result<Dependencies> {
if self.expand_macros {
Self::analyze_with_expansion(package)
} else {
Self::analyze_from_files(package)
}
}
fn analyze_from_files(package: &Package) -> Result<Dependencies> {
let rust_files = Self::get_package_rust_files(package);
let deps_vec: Vec<Dependencies> = rust_files
.par_iter()
.map(|path| Self::process_rust_source(path))
.collect::<Result<Vec<_>>>()?;
Ok(deps_vec.into_iter().fold(FxHashSet::default(), |a, b| a.union(&b).cloned().collect()))
}
fn analyze_with_expansion(package: &Package) -> Result<Dependencies> {
let mut combined_imports = Self::analyze_from_files(package)?;
for target in &package.targets {
let target_arg =
match target.kind.first().ok_or_else(|| anyhow!("Failed to get target kind"))? {
TargetKind::CustomBuild => continue,
TargetKind::Bin => format!("--bin={}", target.name),
TargetKind::Example => format!("--example={}", target.name),
TargetKind::Test => format!("--test={}", target.name),
TargetKind::Bench => format!("--bench={}", target.name),
TargetKind::CDyLib
| TargetKind::DyLib
| TargetKind::Lib
| TargetKind::ProcMacro
| TargetKind::RLib
| TargetKind::StaticLib
| TargetKind::Unknown(_)
| _ => "--lib".to_owned(),
};
let cargo = env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"));
let mut cmd = Command::new(cargo);
cmd.arg("rustc")
.arg(&target_arg)
.arg("--all-features")
.arg("--profile=check")
.arg("--color=never")
.arg("--")
.arg("-Zunpretty=expanded")
.current_dir(package.manifest_path.parent().ok_or_else(|| {
anyhow!("Failed to get parent path: {}", package.manifest_path)
})?);
let output = cmd.output()?;
if !output.status.success() {
return Err(anyhow!(
"Cargo expand failed for {}: {}",
target.name,
String::from_utf8_lossy(&output.stderr)
));
}
let output_str = String::from_utf8(output.stdout)?;
if output_str.is_empty() {
return Err(anyhow!(
"Cargo expand failed for {}: Empty output from cargo expand",
target.name
));
}
let imports = collect_imports(&output_str).map_err(|e| anyhow!("Syntax error: {e}"))?;
combined_imports.extend(imports);
}
Ok(combined_imports)
}
fn get_package_rust_files(package: &Package) -> Vec<PathBuf> {
package
.targets
.iter()
.flat_map(|target| {
if target.kind.contains(&TargetKind::CustomBuild) {
vec![target.src_path.clone().into_std_path_buf()]
} else {
let target_dir = target.src_path.parent().unwrap_or_else(|| {
panic!("Failed to get parent path {}", &target.src_path)
});
WalkDir::new(target_dir)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|e| {
e.file_type().is_file()
&& e.path().extension().is_some_and(|ext| ext == "rs")
})
.map(DirEntry::into_path)
.collect::<Vec<_>>()
}
})
.collect()
}
fn process_rust_source(path: &Path) -> Result<Dependencies> {
let source_text = std::fs::read_to_string(path)?;
collect_imports(&source_text).map_err(|e| anyhow!("Syntax error: {e}"))
}
pub fn parse_package_id(s: &str) -> Result<String> {
if s.contains(' ') {
s.split(' ')
.next()
.map(ToString::to_string)
.ok_or_else(|| anyhow!("Parse error: {s} should have a space"))
} else {
PackageIdSpec::parse(s)
.map(|id| id.name().to_owned())
.map_err(|e| anyhow!("Parse error: {e}"))
}
}
pub fn get_ignored_package_names(value: &serde_json::Value) -> FxHashSet<&str> {
value
.as_object()
.and_then(|object| object.get("cargo-shear"))
.and_then(|object| object.get("ignored"))
.and_then(|ignored| ignored.as_array())
.map(|ignored| {
ignored.iter().filter_map(|item| item.as_str()).collect::<FxHashSet<_>>()
})
.unwrap_or_default()
}
}