use std::path::{Path, PathBuf};
use anyhow::Result;
use rustc_hash::FxHashSet;
use toml::Spanned;
use crate::{
context::{PackageContext, WorkspaceContext},
manifest::{DepLocation, FeatureRef},
package_analyzer::PackageAnalyzer,
};
#[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 UnlinkedFile {
pub path: PathBuf,
}
#[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>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RedundantIgnorePath {
pub pattern: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EmptyFile {
pub path: PathBuf,
}
pub struct PackageProcessor {
expand_macros: bool,
}
#[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 unlinked_files: Vec<UnlinkedFile>,
pub empty_files: Vec<EmptyFile>,
pub unknown_ignores: Vec<UnknownIgnore>,
pub redundant_ignores: Vec<RedundantIgnore>,
pub redundant_ignore_paths: Vec<RedundantIgnorePath>,
}
#[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 { expand_macros }
}
#[expect(
clippy::too_many_lines,
reason = "Complex function handling multiple diagnostic types"
)]
pub fn process_package(&self, ctx: &PackageContext<'_>) -> Result<PackageAnalysis> {
let analyzer = PackageAnalyzer::new(ctx, self.expand_macros);
let used_imports = analyzer.analyze()?;
let code_imports = used_imports.code_imports();
let feature_imports = used_imports.feature_imports();
let mut result = PackageAnalysis::default();
for (import, pkg) in &ctx.import_to_pkg {
if code_imports.contains(import.as_str()) || feature_imports.contains(import.as_str()) {
result.used_packages.insert(pkg.clone());
}
}
let mut suppressed_ignores: FxHashSet<String> = FxHashSet::default();
for (dep, dependency, location) in ctx.manifest.all_dependencies() {
let pkg = dependency.get_ref().package().unwrap_or_else(|| dep.get_ref().as_str());
let import = ctx
.pkg_to_import
.get(pkg)
.cloned()
.unwrap_or_else(|| dep.get_ref().replace('-', "_"));
let is_ignored = ctx.ignored_imports.contains(&import);
if !code_imports.contains(&*import) {
if is_ignored {
suppressed_ignores.insert(import);
continue;
}
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 is_ignored {
suppressed_ignores.insert(import);
continue;
}
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(),
});
}
}
}
let package_ignored_deps = &ctx.manifest.package.metadata.cargo_shear.ignored;
for ignored_dep in package_ignored_deps {
let ignored_import = ignored_dep.get_ref().replace('-', "_");
if !ctx.import_to_pkg.contains_key(&ignored_import) {
result.unknown_ignores.push(UnknownIgnore { name: ignored_dep.clone() });
continue;
}
if !suppressed_ignores.contains(&ignored_import) {
result.redundant_ignores.push(RedundantIgnore { name: ignored_dep.clone() });
}
}
let unlinked_files: FxHashSet<PathBuf> = used_imports
.unlinked_files
.iter()
.filter_map(|path| path.strip_prefix(&ctx.directory).ok().map(Path::to_path_buf))
.collect();
let pkg_ignored_paths = &ctx.manifest.package.metadata.cargo_shear.ignored_paths;
let ws_ignored_paths = &ctx.workspace.manifest.workspace.metadata.cargo_shear.ignored_paths;
let root = ctx.directory.strip_prefix(&ctx.workspace.root).unwrap_or(&ctx.directory);
result.redundant_ignore_paths = pkg_ignored_paths
.iter()
.filter(|matcher| !unlinked_files.iter().any(|path| matcher.is_match(path)))
.map(|matcher| RedundantIgnorePath { pattern: matcher.glob().glob().to_owned() })
.collect();
result.unlinked_files = unlinked_files
.into_iter()
.filter(|path| {
!pkg_ignored_paths.iter().any(|globs| globs.is_match(path))
&& !ws_ignored_paths.iter().any(|globs| globs.is_match(root.join(path)))
})
.map(|path| UnlinkedFile { path })
.collect();
result.empty_files = used_imports
.empty_files
.iter()
.filter_map(|path| path.strip_prefix(&ctx.directory).ok().map(Path::to_path_buf))
.filter(|path| {
!pkg_ignored_paths.iter().any(|globs| globs.is_match(path))
&& !ws_ignored_paths.iter().any(|globs| globs.is_match(root.join(path)))
})
.map(|path| EmptyFile { path })
.collect();
Ok(result)
}
pub fn process_workspace(
ctx: &WorkspaceContext,
workspace_used_pkgs: &FxHashSet<String>,
) -> WorkspaceAnalysis {
let mut result = WorkspaceAnalysis::default();
if ctx.packages <= 1 {
return result;
}
if ctx.manifest.workspace.dependencies.is_empty() {
return result;
}
for (dep, dependency) in &ctx.manifest.workspace.dependencies {
if ctx.ignored_deps.contains(dep.get_ref()) {
continue;
}
let pkg = dependency.get_ref().package().unwrap_or(dep.get_ref());
if !workspace_used_pkgs.contains(pkg) {
result.unused_dependencies.push(UnusedWorkspaceDependency { name: dep.clone() });
}
}
let ignored_deps = &ctx.manifest.workspace.metadata.cargo_shear.ignored;
for ignored_dep in ignored_deps {
if !ctx.dep_to_pkg.contains_key(ignored_dep.get_ref()) {
result.unknown_ignores.push(UnknownIgnore { name: ignored_dep.clone() });
continue;
}
if ctx
.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
}
}