use std::borrow::Cow;
use anyhow::{Result, anyhow};
use cargo_metadata::{Metadata, NodeDep, Package};
use rustc_hash::{FxHashMap, FxHashSet};
use toml::Spanned;
use crate::{
dependency_analyzer::{DependencyAnalyzer, FeatureRef},
manifest::{DepLocation, DepsSet, Manifest},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnusedDependency {
pub name: Spanned<String>,
pub location: DepLocation,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnusedOptionalDependency {
pub name: Spanned<String>,
pub features: Vec<FeatureRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnusedFeatureDependency {
pub name: Spanned<String>,
pub features: Vec<FeatureRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnusedWorkspaceDependency {
pub name: Spanned<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MisplacedDependency {
pub name: Spanned<String>,
pub location: DepLocation,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MisplacedOptionalDependency {
pub name: Spanned<String>,
pub location: DepLocation,
pub features: Vec<FeatureRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnknownIgnore {
pub name: Spanned<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RedundantIgnore {
pub name: Spanned<String>,
}
pub struct PackageProcessor {
analyzer: DependencyAnalyzer,
}
#[derive(Default)]
pub struct PackageAnalysis {
pub used_packages: FxHashSet<String>,
pub unused_dependencies: Vec<UnusedDependency>,
pub unused_optional_dependencies: Vec<UnusedOptionalDependency>,
pub unused_feature_dependencies: Vec<UnusedFeatureDependency>,
pub misplaced_dependencies: Vec<MisplacedDependency>,
pub misplaced_optional_dependencies: Vec<MisplacedOptionalDependency>,
pub unknown_ignores: Vec<UnknownIgnore>,
pub redundant_ignores: Vec<RedundantIgnore>,
}
#[derive(Default)]
pub struct WorkspaceAnalysis {
pub unused_dependencies: Vec<UnusedWorkspaceDependency>,
pub unknown_ignores: Vec<UnknownIgnore>,
pub redundant_ignores: Vec<RedundantIgnore>,
}
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,
manifest: &Manifest,
workspace_manifest: &Manifest,
) -> Result<PackageAnalysis> {
let mut result = PackageAnalysis::default();
let package_ignored_deps = &manifest.package.metadata.cargo_shear.ignored;
let workspace_ignored_deps = &workspace_manifest.workspace.metadata.cargo_shear.ignored;
let resolved = 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 import_to_pkg = Self::import_to_pkg_map(metadata, &resolved.deps)?;
let pkg_to_import = Self::pkg_to_import_map(&import_to_pkg);
let used_imports = self.analyzer.analyze_package(package, manifest)?;
let code_imports = used_imports.code_imports();
let feature_imports = used_imports.feature_imports();
let ignored_imports: FxHashSet<String> = package_ignored_deps
.iter()
.chain(workspace_ignored_deps)
.map(|dep| dep.get_ref().replace('-', "_"))
.collect();
for (&import, &pkg) in &import_to_pkg {
if code_imports.contains(import) || feature_imports.contains(import) {
result.used_packages.insert(pkg.to_owned());
}
}
for (dep, dependency, location) in manifest.all_dependencies() {
let pkg = dependency.get_ref().package().unwrap_or_else(|| dep.get_ref().as_str());
let import = Self::resolve_import_name(&pkg_to_import, dep.get_ref(), pkg);
if ignored_imports.contains(&*import) {
continue;
}
if !code_imports.contains(&*import) {
if dependency.get_ref().optional() {
result.unused_optional_dependencies.push(UnusedOptionalDependency {
name: dep.clone(),
features: used_imports.features.get(&*import).cloned().unwrap_or_default(),
});
continue;
}
if feature_imports.contains(&*import) {
result.unused_feature_dependencies.push(UnusedFeatureDependency {
name: dep.clone(),
features: used_imports.features.get(&*import).cloned().unwrap_or_default(),
});
continue;
}
result
.unused_dependencies
.push(UnusedDependency { name: dep.clone(), location: location.clone() });
continue;
}
if location.is_normal()
&& !used_imports.normal.contains(&*import)
&& used_imports.dev.contains(&*import)
{
if dependency.get_ref().optional() {
result.misplaced_optional_dependencies.push(MisplacedOptionalDependency {
name: dep.clone(),
location: location.clone(),
features: used_imports.features.get(&*import).cloned().unwrap_or_default(),
});
} else {
result.misplaced_dependencies.push(MisplacedDependency {
name: dep.clone(),
location: location.clone(),
});
}
}
}
for ignored_dep in package_ignored_deps {
let ignored_import = ignored_dep.get_ref().replace('-', "_");
if !import_to_pkg.contains_key(ignored_import.as_str()) {
result.unknown_ignores.push(UnknownIgnore { name: ignored_dep.clone() });
continue;
}
if code_imports.contains(&ignored_import) {
result.redundant_ignores.push(RedundantIgnore { name: ignored_dep.clone() });
}
}
Ok(result)
}
pub fn process_workspace(
manifest: &Manifest,
metadata: &Metadata,
workspace_used_pkgs: &FxHashSet<String>,
) -> WorkspaceAnalysis {
let mut result = WorkspaceAnalysis::default();
if metadata.workspace_packages().len() <= 1 {
return result;
}
if manifest.workspace.dependencies.is_empty() {
return result;
}
let ignored_deps = &manifest.workspace.metadata.cargo_shear.ignored;
let ignored_dep_keys: FxHashSet<&str> =
ignored_deps.iter().map(|s| s.get_ref().as_str()).collect();
for (dep, dependency) in &manifest.workspace.dependencies {
if ignored_dep_keys.contains(dep.get_ref().as_str()) {
continue;
}
let pkg =
dependency.get_ref().package().map_or_else(|| dep.get_ref().clone(), str::to_owned);
if !workspace_used_pkgs.contains(&pkg) {
result.unused_dependencies.push(UnusedWorkspaceDependency { name: dep.clone() });
}
}
let dep_to_pkg = Self::dep_to_pkg_map(&manifest.workspace.dependencies);
for ignored_dep in ignored_deps {
if !dep_to_pkg.contains_key(ignored_dep.get_ref()) {
result.unknown_ignores.push(UnknownIgnore { name: ignored_dep.clone() });
continue;
}
if dep_to_pkg
.get(ignored_dep.get_ref())
.is_some_and(|pkg| workspace_used_pkgs.contains(pkg))
{
result.redundant_ignores.push(RedundantIgnore { name: ignored_dep.clone() });
}
}
result
}
fn import_to_pkg_map<'a>(
metadata: &'a Metadata,
imports: &'a [NodeDep],
) -> Result<FxHashMap<&'a str, &'a str>> {
imports
.iter()
.map(|import| {
let pkg = metadata
.packages
.iter()
.find(|p| p.id == import.pkg)
.ok_or_else(|| anyhow!("Package not found: {}", import.pkg.repr))?;
Ok((import.name.as_str(), pkg.name.as_str()))
})
.collect()
}
fn pkg_to_import_map<'a>(
import_to_pkg: &FxHashMap<&'a str, &'a str>,
) -> FxHashMap<&'a str, &'a str> {
import_to_pkg.iter().map(|(&import, &pkg)| (pkg, import)).collect()
}
fn dep_to_pkg_map(deps: &DepsSet) -> FxHashMap<String, String> {
deps.iter()
.map(|(dep, dependency)| {
let dep = dep.get_ref();
let pkg = dependency.get_ref().package().map_or_else(|| dep.clone(), str::to_owned);
(dep.clone(), pkg)
})
.collect()
}
fn resolve_import_name<'a>(
pkg_to_import: &'a FxHashMap<&str, &str>,
dep: &'a str,
pkg: &str,
) -> Cow<'a, str> {
pkg_to_import
.get(pkg)
.map_or_else(|| Cow::Owned(dep.replace('-', "_")), |&import| Cow::Borrowed(import))
}
}