mod cargo_toml_editor;
mod dependency_analyzer;
mod diagnostics;
mod import_collector;
mod manifest;
mod output;
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, MetadataCommand};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use toml_edit::DocumentMut;
pub use crate::output::{ColorMode, OutputFormat};
use crate::{
cargo_toml_editor::CargoTomlEditor,
diagnostics::ShearAnalysis,
manifest::Manifest,
output::Renderer,
package_processor::{PackageAnalysis, PackageProcessor, WorkspaceAnalysis},
};
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(long, fallback(OutputFormat::Auto))]
format: OutputFormat,
#[bpaf(long, fallback(ColorMode::Auto))]
color: ColorMode,
#[bpaf(positional("PATH"), fallback_with(default_path))]
path: PathBuf,
}
impl CargoShearOptions {
#[must_use]
pub fn new(path: PathBuf) -> Self {
Self {
path,
fix: false,
expand: false,
locked: false,
offline: false,
frozen: false,
package: vec![],
exclude: vec![],
format: OutputFormat::default(),
color: ColorMode::default(),
}
}
#[must_use]
pub const fn with_fix(mut self) -> Self {
self.fix = true;
self
}
#[must_use]
pub const fn with_expand(mut self) -> Self {
self.expand = true;
self
}
#[must_use]
pub const fn with_locked(mut self) -> Self {
self.locked = true;
self
}
#[must_use]
pub const fn with_offline(mut self) -> Self {
self.offline = true;
self
}
#[must_use]
pub const fn with_frozen(mut self) -> Self {
self.frozen = true;
self
}
#[must_use]
pub fn with_packages(mut self, packages: Vec<String>) -> Self {
self.package = packages;
self
}
#[must_use]
pub fn with_excludes(mut self, excludes: Vec<String>) -> Self {
self.exclude = excludes;
self
}
#[must_use]
pub const fn with_format(mut self, format: OutputFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub const fn with_color(mut self, color: ColorMode) -> Self {
self.color = color;
self
}
}
pub(crate) fn default_path() -> Result<PathBuf> {
Ok(env::current_dir()?)
}
pub struct CargoShear<W> {
writer: W,
options: CargoShearOptions,
analysis: ShearAnalysis,
}
impl<W: Write> CargoShear<W> {
#[must_use]
pub fn new(writer: W, options: CargoShearOptions) -> Self {
Self { writer, options, analysis: ShearAnalysis::default() }
}
#[must_use]
pub fn run(mut self) -> ExitCode {
match self.shear() {
Ok(()) => {
let color = self.options.color.enabled();
let mut renderer = Renderer::new(&mut self.writer, self.options.format, color);
if let Err(err) = renderer.render(&self.analysis) {
let _ = writeln!(self.writer, "error rendering report: {err:?}");
return ExitCode::from(2);
}
let has_errors = self.analysis.unused > 0 || self.analysis.misplaced > 0;
if self.options.fix && self.analysis.fixed > 0 && !has_errors {
ExitCode::SUCCESS
} else if has_errors {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
Err(err) => {
let _ = writeln!(self.writer, "error: {err:?}");
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 root = metadata.workspace_root.as_std_path().to_path_buf();
let workspace_manifest_path = metadata.workspace_root.as_std_path().join("Cargo.toml");
let workspace_content = fs::read_to_string(&workspace_manifest_path)?;
let workspace_manifest: Manifest = toml::from_str(&workspace_content)?;
let packages = metadata.workspace_packages();
let packages: Vec<_> = packages
.into_iter()
.filter(|package| {
if self.options.exclude.iter().any(|name| name == package.name.as_str()) {
return false;
}
if !self.options.package.is_empty()
&& !self.options.package.iter().any(|name| name == package.name.as_str())
{
return false;
}
true
})
.collect();
let results: Vec<_> = packages
.par_iter()
.map(|package| {
let manifest_path = package.manifest_path.as_std_path();
let relative_path = manifest_path.strip_prefix(&root).unwrap_or(manifest_path);
let content = fs::read_to_string(manifest_path)?;
let manifest: Manifest = toml::from_str(&content)?;
let result = processor.process_package(
&metadata,
package,
&manifest,
&workspace_manifest,
)?;
Ok::<_, anyhow::Error>((relative_path.to_path_buf(), content, result))
})
.collect::<Result<Vec<_>>>()?;
for (path, content, result) in results {
let absolute_path = root.join(&path);
let fixed = self.fix_package_issues(&absolute_path, &result)?;
self.analysis.add_package_result(&path, content, &result, fixed);
}
if self.options.package.is_empty() && self.options.exclude.is_empty() {
let workspace_result = PackageProcessor::process_workspace(
&workspace_manifest,
&metadata,
&self.analysis.packages,
);
let relative =
workspace_manifest_path.strip_prefix(&root).unwrap_or(&workspace_manifest_path);
let fixed = self.fix_workspace_issues(&workspace_manifest_path, &workspace_result)?;
self.analysis.add_workspace_result(
relative,
workspace_content,
&workspace_result,
fixed,
);
}
Ok(())
}
fn fix_package_issues(&self, manifest_path: &Path, result: &PackageAnalysis) -> Result<usize> {
if !self.options.fix {
return Ok(0);
}
if result.misplaced_dependencies.is_empty() && result.unused_dependencies.is_empty() {
return Ok(0);
}
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())?;
Ok(fixed_unused + fixed_misplaced)
}
fn fix_workspace_issues(
&self,
manifest_path: &Path,
result: &WorkspaceAnalysis,
) -> Result<usize> {
if !self.options.fix {
return Ok(0);
}
if result.unused_dependencies.is_empty() {
return Ok(0);
}
let content = fs::read_to_string(manifest_path)?;
let mut manifest = DocumentMut::from_str(&content)?;
let fixed =
CargoTomlEditor::remove_workspace_deps(&mut manifest, &result.unused_dependencies);
fs::write(manifest_path, manifest.to_string())?;
Ok(fixed)
}
}