use rustc_hash::{FxHashMap, FxHashSet};
use std::env;
use std::path::{Path, PathBuf};
use cargo_metadata::{Metadata, Package};
use crate::dependency_analyzer::{Dependencies, DependencyAnalyzer};
use anyhow::{Result, anyhow};
pub struct PackageProcessor {
analyzer: DependencyAnalyzer,
}
pub struct ProcessResult {
pub unused_dependencies: FxHashSet<String>,
pub remaining_dependencies: Dependencies,
}
impl PackageProcessor {
pub const fn new(expand_macros: bool) -> Self {
Self { analyzer: DependencyAnalyzer::new(expand_macros) }
}
pub fn process_package(&self, metadata: &Metadata, package: &Package) -> Result<ProcessResult> {
let package_ignored_names =
DependencyAnalyzer::get_ignored_package_names(&package.metadata);
let workspace_ignored_names =
DependencyAnalyzer::get_ignored_package_names(&metadata.workspace_metadata);
let mut ignored_names = package_ignored_names.clone();
ignored_names.extend(workspace_ignored_names.clone());
let this_package = metadata
.resolve
.as_ref()
.ok_or_else(|| {
anyhow!("`cargo_metadata::MetadataCommand::no_deps` should not be called.")
})?
.nodes
.iter()
.find(|node| node.id == package.id)
.ok_or_else(|| anyhow!("Package not found: {}", package.name))?;
let all_package_dep_names: FxHashSet<String> = this_package
.deps
.iter()
.map(|node_dep| {
DependencyAnalyzer::parse_package_id(&node_dep.pkg.repr)
.unwrap_or_else(|_| node_dep.name.clone())
})
.collect();
for ignored in &package_ignored_names {
if !all_package_dep_names.contains(*ignored) {
println!(
"warning: '{ignored}' is redundant in [package.metadata.cargo-shear] for package '{}'.\n",
package.name
);
}
}
let package_dependency_names_map =
Self::build_dependency_map(&this_package.deps, &ignored_names)?;
let module_names_from_package_deps: FxHashSet<String> =
package_dependency_names_map.keys().cloned().collect();
let package_dependency_names: FxHashSet<String> = package_dependency_names_map
.values()
.cloned()
.chain(package_ignored_names.into_iter().map(str::to_string))
.collect();
let module_names_from_rust_files = self.analyzer.analyze_package(package)?;
let unused_module_names: Vec<&String> =
module_names_from_package_deps.difference(&module_names_from_rust_files).collect();
if unused_module_names.is_empty() {
return Ok(ProcessResult {
unused_dependencies: FxHashSet::default(),
remaining_dependencies: package_dependency_names,
});
}
let unused_dependency_names: FxHashSet<String> = unused_module_names
.into_iter()
.map(|name| package_dependency_names_map[name].clone())
.collect();
let remaining_dependencies =
package_dependency_names.difference(&unused_dependency_names).cloned().collect();
Ok(ProcessResult { unused_dependencies: unused_dependency_names, remaining_dependencies })
}
pub fn process_workspace(
metadata: &Metadata,
all_package_deps: &Dependencies,
) -> Result<FxHashSet<String>> {
if metadata.workspace_packages().len() <= 1 {
return Ok(FxHashSet::default());
}
let metadata_path = metadata.workspace_root.as_std_path();
let cargo_toml_path = metadata_path.join("Cargo.toml");
let manifest = cargo_toml::Manifest::from_path(&cargo_toml_path)?;
let Some(workspace) = &manifest.workspace else {
return Ok(FxHashSet::default());
};
let ignored_names =
DependencyAnalyzer::get_ignored_package_names(&metadata.workspace_metadata);
let all_workspace_dep_names: FxHashSet<String> = workspace
.dependencies
.iter()
.map(|(key, dependency)| {
dependency
.detail()
.and_then(|detail| detail.package.as_ref())
.unwrap_or(key)
.clone()
})
.collect();
for ignored in &ignored_names {
if !all_workspace_dep_names.contains(*ignored) {
println!(
"warning: '{ignored}' is redundant in [workspace.metadata.cargo-shear].\n"
);
}
}
let workspace_deps: FxHashSet<String> = all_workspace_dep_names
.into_iter()
.filter(|name| !ignored_names.contains(name.as_str()))
.collect();
Ok(workspace_deps.difference(all_package_deps).cloned().collect())
}
pub fn get_relative_path(manifest_path: &Path, workspace_root: &Path) -> PathBuf {
let dir = manifest_path.parent().unwrap_or(manifest_path);
let current_dir = env::current_dir().unwrap_or_default();
manifest_path
.strip_prefix(¤t_dir)
.or_else(|_| manifest_path.strip_prefix(workspace_root))
.unwrap_or(dir)
.to_path_buf()
}
fn build_dependency_map(
deps: &[cargo_metadata::NodeDep],
ignored_names: &FxHashSet<&str>,
) -> Result<FxHashMap<String, String>> {
Ok(deps
.iter()
.map(|node_dep| {
DependencyAnalyzer::parse_package_id(&node_dep.pkg.repr)
.map(|package_name| (node_dep.name.clone(), package_name))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.filter(|(_, name)| !ignored_names.contains(name.as_str()))
.collect::<FxHashMap<_, _>>())
}
}