mod cargo_toml_editor;
mod dependency_analyzer;
mod import_collector;
mod package_processor;
#[cfg(test)]
mod tests;
use std::{
env, fs,
io::Write,
path::{Path, PathBuf},
process::ExitCode,
str::FromStr,
};
use anyhow::Result;
use bpaf::Bpaf;
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Package};
use cargo_toml::Manifest;
use rustc_hash::FxHashSet;
use toml_edit::DocumentMut;
use crate::{
cargo_toml_editor::CargoTomlEditor,
package_processor::{PackageProcessResult, PackageProcessor, WorkspaceProcessResult},
};
const VERSION: &str = match option_env!("SHEAR_VERSION") {
Some(v) => v,
None => "dev",
};
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options("shear"), version(VERSION))]
pub struct CargoShearOptions {
#[bpaf(long)]
fix: bool,
#[bpaf(long)]
expand: bool,
locked: bool,
offline: bool,
frozen: bool,
#[bpaf(long, short, argument("SPEC"))]
package: Vec<String>,
exclude: Vec<String>,
#[bpaf(positional("PATH"), fallback_with(default_path))]
path: PathBuf,
}
impl CargoShearOptions {
#[must_use]
pub const fn new_for_test(path: PathBuf, fix: bool) -> Self {
Self {
fix,
expand: false,
locked: false,
offline: false,
frozen: false,
package: vec![],
exclude: vec![],
path,
}
}
}
pub(crate) fn default_path() -> Result<PathBuf> {
Ok(env::current_dir()?)
}
pub struct CargoShear<W> {
writer: W,
options: CargoShearOptions,
unused_dependencies: usize,
misplaced_dependencies: usize,
fixed_dependencies: usize,
}
impl<W: Write> CargoShear<W> {
#[must_use]
pub const fn new(writer: W, options: CargoShearOptions) -> Self {
Self {
writer,
options,
unused_dependencies: 0,
misplaced_dependencies: 0,
fixed_dependencies: 0,
}
}
#[must_use]
pub fn run(mut self) -> ExitCode {
let _ = writeln!(self.writer, "Analyzing {}", self.options.path.to_string_lossy());
let _ = writeln!(self.writer);
match self.shear() {
Ok(()) => {
let has_fixed = self.fixed_dependencies > 0;
if has_fixed {
let _ = writeln!(
self.writer,
"Fixed {} {}.\n",
self.fixed_dependencies,
if self.fixed_dependencies == 1 { "dependency" } else { "dependencies" }
);
}
let total_issues = self.unused_dependencies + self.misplaced_dependencies;
let has_issues = (total_issues - self.fixed_dependencies) > 0;
if has_issues {
let _ = writeln!(
self.writer,
"\n\
cargo-shear may have detected unused dependencies incorrectly due to its limitations.\n\
They can be ignored by adding the crate name to the package's Cargo.toml:\n\n\
[package.metadata.cargo-shear]\n\
ignored = [\"crate-name\"]\n\n\
or in the workspace Cargo.toml:\n\n\
[workspace.metadata.cargo-shear]\n\
ignored = [\"crate-name\"]\n"
);
if !self.options.fix {
let _ =
writeln!(self.writer, "To automatically fix issues, run with --fix");
}
} else {
let _ = writeln!(self.writer, "No issues detected!");
}
ExitCode::from(u8::from(if self.options.fix { has_fixed } else { has_issues }))
}
Err(err) => {
let _ = writeln!(self.writer, "{err:?}");
let _ = writeln!(
self.writer,
"note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace"
);
ExitCode::from(2)
}
}
}
fn shear(&mut self) -> Result<()> {
let mut extra_opts = Vec::new();
if self.options.locked {
extra_opts.push("--locked".to_owned());
}
if self.options.offline {
extra_opts.push("--offline".to_owned());
}
if self.options.frozen {
extra_opts.push("--frozen".to_owned());
}
let metadata = MetadataCommand::new()
.features(CargoOpt::AllFeatures)
.current_dir(&self.options.path)
.other_options(extra_opts)
.exec()
.map_err(|e| anyhow::anyhow!("Metadata error: {e}"))?;
let processor = PackageProcessor::new(self.options.expand);
let mut workspace_used_pkgs = FxHashSet::default();
for package in metadata.workspace_packages() {
if self.options.exclude.iter().any(|name| name == package.name.as_str()) {
continue;
}
if !self.options.package.is_empty()
&& !self.options.package.iter().any(|name| name == package.name.as_str())
{
continue;
}
let manifest_path = package.manifest_path.as_std_path();
let manifest = Manifest::from_path(manifest_path)?;
let result = processor.process_package(&metadata, package, &manifest)?;
self.report_package_issues(package, &result, &metadata)?;
self.fix_package_issues(manifest_path, &result)?;
workspace_used_pkgs.extend(result.used_packages);
}
let manifest_path = metadata.workspace_root.as_std_path().join("Cargo.toml");
let workspace_manifest = Manifest::from_path(&manifest_path)?;
let workspace_result = PackageProcessor::process_workspace(
&workspace_manifest,
&metadata,
&workspace_used_pkgs,
);
self.report_workspace_issues(&manifest_path, &workspace_result)?;
self.fix_workspace_issues(&manifest_path, &workspace_result)?;
Ok(())
}
fn report_package_issues(
&mut self,
package: &Package,
result: &PackageProcessResult,
metadata: &Metadata,
) -> Result<()> {
for ignored_dep in &result.redundant_ignores {
writeln!(
self.writer,
"warning: '{ignored_dep}' is redundant in [package.metadata.cargo-shear] for package '{}'.\n",
package.name
)?;
}
let unused_count = result.unused_dependencies.len();
let misplaced_count = result.misplaced_dependencies.len();
if unused_count == 0 && misplaced_count == 0 {
return Ok(());
}
let relative_path = PackageProcessor::get_relative_path(
package.manifest_path.as_std_path(),
metadata.workspace_root.as_std_path(),
);
writeln!(self.writer, "{} -- {}:", package.name, relative_path.display())?;
if unused_count > 0 {
writeln!(self.writer, " unused dependencies:")?;
for misplaced_dep in &result.unused_dependencies {
writeln!(self.writer, " {misplaced_dep}")?;
}
}
if misplaced_count > 0 {
writeln!(self.writer, " move to dev-dependencies:")?;
for misplaced_dep in &result.misplaced_dependencies {
writeln!(self.writer, " {misplaced_dep}")?;
}
}
writeln!(self.writer)?;
self.unused_dependencies += unused_count;
self.misplaced_dependencies += misplaced_count;
Ok(())
}
fn fix_package_issues(
&mut self,
manifest_path: &Path,
result: &PackageProcessResult,
) -> Result<()> {
if !self.options.fix {
return Ok(());
}
if result.misplaced_dependencies.is_empty() && result.unused_dependencies.is_empty() {
return Ok(());
}
let content = fs::read_to_string(manifest_path)?;
let mut manifest = DocumentMut::from_str(&content)?;
let fixed_unused =
CargoTomlEditor::remove_dependencies(&mut manifest, &result.unused_dependencies);
let fixed_misplaced = CargoTomlEditor::move_to_dev_dependencies(
&mut manifest,
&result.misplaced_dependencies,
);
fs::write(manifest_path, manifest.to_string())?;
self.fixed_dependencies += fixed_unused + fixed_misplaced;
Ok(())
}
fn report_workspace_issues(
&mut self,
manifest_path: &Path,
result: &WorkspaceProcessResult,
) -> Result<()> {
for ignored_dep in &result.redundant_ignores {
writeln!(
self.writer,
"warning: '{ignored_dep}' is redundant in [workspace.metadata.cargo-shear].\n"
)?;
}
if result.unused_dependencies.is_empty() {
return Ok(());
}
let path = manifest_path
.strip_prefix(env::current_dir().unwrap_or_default())
.unwrap_or(manifest_path)
.to_string_lossy();
writeln!(self.writer, "root -- {path}:")?;
writeln!(self.writer, " unused dependencies:")?;
for unused_dep in &result.unused_dependencies {
writeln!(self.writer, " {unused_dep}")?;
}
writeln!(self.writer)?;
self.unused_dependencies += result.unused_dependencies.len();
Ok(())
}
fn fix_workspace_issues(
&mut self,
manifest_path: &Path,
result: &WorkspaceProcessResult,
) -> Result<()> {
if !self.options.fix {
return Ok(());
}
if result.unused_dependencies.is_empty() {
return Ok(());
}
let content = fs::read_to_string(manifest_path)?;
let mut manifest = DocumentMut::from_str(&content)?;
let fixed =
CargoTomlEditor::remove_dependencies(&mut manifest, &result.unused_dependencies);
fs::write(manifest_path, manifest.to_string())?;
self.fixed_dependencies += fixed;
Ok(())
}
}