use std::path::{Path, PathBuf};
use anyhow::Result;
use rustc_hash::FxHashSet;
use toml::Spanned;
use cargo_metadata::TargetKind;
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)]
pub struct RedundantIgnorePath {
pub pattern: Spanned<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EmptyFile {
pub path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TestDisabledWithTests {
pub target_name: String,
pub target_kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TestEnabledWithoutTests {
pub target_name: String,
pub target_kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DoctestDisabledWithDoctests {
pub target_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DoctestEnabledWithoutDoctests {
pub target_name: String,
}
pub struct PackageProcessor {
expand_macros: bool,
check_test_targets: 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>,
pub used_workspace_ignore_paths: FxHashSet<String>,
pub test_disabled_with_tests: Vec<TestDisabledWithTests>,
pub test_enabled_without_tests: Vec<TestEnabledWithoutTests>,
pub doctest_disabled_with_doctests: Vec<DoctestDisabledWithDoctests>,
pub doctest_enabled_without_doctests: Vec<DoctestEnabledWithoutDoctests>,
}
impl PackageAnalysis {
pub const fn has_fixable_issues(&self) -> bool {
!self.misplaced_dependencies.is_empty()
|| !self.unused_dependencies.is_empty()
|| !self.test_disabled_with_tests.is_empty()
|| !self.test_enabled_without_tests.is_empty()
|| !self.doctest_disabled_with_doctests.is_empty()
|| !self.doctest_enabled_without_doctests.is_empty()
}
}
#[derive(Default)]
pub struct WorkspaceAnalysis {
pub unused_dependencies: Vec<UnusedWorkspaceDependency>,
pub unknown_ignores: Vec<UnknownIgnore>,
pub redundant_ignores: Vec<RedundantIgnore>,
pub redundant_ignore_paths: Vec<RedundantIgnorePath>,
}
impl PackageProcessor {
pub const fn new(expand_macros: bool, check_test_targets: bool) -> Self {
Self { expand_macros, check_test_targets }
}
#[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 {
if !ctx.workspace.ignored_deps.contains(dep.get_ref().as_str()) {
result.used_packages.insert(pkg.to_owned());
}
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 empty_files: FxHashSet<PathBuf> = used_imports
.empty_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(|glob| {
!unlinked_files.iter().any(|path| glob.matcher.is_match(path))
&& !empty_files.iter().any(|path| glob.matcher.is_match(path))
})
.map(|glob| RedundantIgnorePath { pattern: glob.pattern.clone() })
.collect();
for glob in ws_ignored_paths {
let matches_unlinked = unlinked_files.iter().any(|path| {
let not_matched_by_pkg =
!pkg_ignored_paths.iter().any(|pkg| pkg.matcher.is_match(path));
not_matched_by_pkg && glob.matcher.is_match(root.join(path))
});
let matches_empty = empty_files.iter().any(|path| {
let not_matched_by_pkg =
!pkg_ignored_paths.iter().any(|pkg| pkg.matcher.is_match(path));
not_matched_by_pkg && glob.matcher.is_match(root.join(path))
});
if matches_unlinked || matches_empty {
result.used_workspace_ignore_paths.insert(glob.pattern.get_ref().clone());
}
}
result.unlinked_files = unlinked_files
.into_iter()
.filter(|path| {
!pkg_ignored_paths.iter().any(|glob| glob.matcher.is_match(path))
&& !ws_ignored_paths.iter().any(|glob| glob.matcher.is_match(root.join(path)))
})
.map(|path| UnlinkedFile { path })
.collect();
result.empty_files = empty_files
.into_iter()
.filter(|path| {
!pkg_ignored_paths.iter().any(|glob| glob.matcher.is_match(path))
&& !ws_ignored_paths.iter().any(|glob| glob.matcher.is_match(root.join(path)))
})
.map(|path| EmptyFile { path })
.collect();
if self.check_test_targets {
let is_workspace = ctx.workspace.packages.len() > 1;
for info in &used_imports.target_test_info {
#[expect(
clippy::wildcard_enum_match_arm,
reason = "Only lib-like targets reach here"
)]
let kind_str = match &info.target_kind {
TargetKind::CDyLib => "cdylib",
TargetKind::DyLib => "dylib",
TargetKind::ProcMacro => "proc-macro",
TargetKind::RLib => "rlib",
TargetKind::StaticLib => "staticlib",
_ => "lib",
};
if !info.test_enabled && info.has_tests {
result.test_disabled_with_tests.push(TestDisabledWithTests {
target_name: info.target_name.clone(),
target_kind: kind_str.to_owned(),
});
}
if is_workspace && info.test_enabled && !info.has_tests {
result.test_enabled_without_tests.push(TestEnabledWithoutTests {
target_name: info.target_name.clone(),
target_kind: kind_str.to_owned(),
});
}
if !info.doctest_enabled && info.has_doctests {
result.doctest_disabled_with_doctests.push(DoctestDisabledWithDoctests {
target_name: info.target_name.clone(),
});
}
if is_workspace && info.doctest_enabled && !info.has_doctests {
result.doctest_enabled_without_doctests.push(DoctestEnabledWithoutDoctests {
target_name: info.target_name.clone(),
});
}
}
}
Ok(result)
}
pub fn process_workspace(
ctx: &WorkspaceContext,
workspace_used_pkgs: &FxHashSet<String>,
used_workspace_ignore_paths: &FxHashSet<String>,
) -> WorkspaceAnalysis {
let mut result = WorkspaceAnalysis::default();
let ws_ignored_paths = &ctx.manifest.workspace.metadata.cargo_shear.ignored_paths;
for glob in ws_ignored_paths {
if !used_workspace_ignore_paths.contains(glob.pattern.get_ref()) {
result
.redundant_ignore_paths
.push(RedundantIgnorePath { pattern: glob.pattern.clone() });
}
}
if ctx.packages.len() <= 1 || 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
}
}