mod cargo_toml_editor;
mod dependency_analyzer;
mod import_collector;
mod package_processor;
#[cfg(test)]
mod tests;
use std::{env, io::Write, path::PathBuf, process::ExitCode};
use anyhow::Result;
use bpaf::Bpaf;
use cargo_metadata::{CargoOpt, MetadataCommand};
use cargo_toml::Manifest;
use rustc_hash::FxHashSet;
use crate::{cargo_toml_editor::CargoTomlEditor, package_processor::PackageProcessor};
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,
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, 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 has_deps = (self.unused_dependencies - self.fixed_dependencies) > 0;
if has_deps {
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"
);
} else {
let _ = writeln!(self.writer, "No unused dependencies!");
}
ExitCode::from(u8::from(if self.options.fix { has_fixed } else { has_deps }))
}
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 = Manifest::from_path(package.manifest_path.as_std_path())?;
let result = processor.process_package(&metadata, package, &manifest)?;
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
)?;
}
if !result.unused_dependencies.is_empty() {
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())?;
for unused_dep in &result.unused_dependencies {
writeln!(self.writer, " {unused_dep}")?;
}
writeln!(self.writer)?;
self.unused_dependencies += result.unused_dependencies.len();
if self.options.fix {
let fixed = CargoTomlEditor::remove_dependencies(
package.manifest_path.as_std_path(),
&result.unused_dependencies,
)?;
self.fixed_dependencies += fixed;
}
}
workspace_used_pkgs.extend(result.used_packages);
}
let cargo_toml_path = metadata.workspace_root.as_std_path().join("Cargo.toml");
let manifest = Manifest::from_path(&cargo_toml_path)?;
let workspace_result =
PackageProcessor::process_workspace(&manifest, &metadata, &workspace_used_pkgs);
for ignored_dep in &workspace_result.redundant_ignores {
writeln!(
self.writer,
"warning: '{ignored_dep}' is redundant in [workspace.metadata.cargo-shear].\n"
)?;
}
if !workspace_result.unused_dependencies.is_empty() {
let path = cargo_toml_path
.strip_prefix(env::current_dir().unwrap_or_default())
.unwrap_or(&cargo_toml_path)
.to_string_lossy();
writeln!(self.writer, "root -- {path}:")?;
for unused_dep in &workspace_result.unused_dependencies {
writeln!(self.writer, " {unused_dep}")?;
}
writeln!(self.writer)?;
self.unused_dependencies += workspace_result.unused_dependencies.len();
if self.options.fix {
let fixed = CargoTomlEditor::remove_dependencies(
&cargo_toml_path,
&workspace_result.unused_dependencies,
)?;
self.fixed_dependencies += fixed;
}
}
Ok(())
}
}