mod cargo_toml_editor;
mod context;
mod diagnostics;
mod manifest;
mod output;
mod package_analyzer;
mod package_processor;
mod source_parser;
#[cfg(test)]
mod tests;
pub mod util;
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 owo_colors::OwoColorize;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rustc_hash::FxHashSet;
use toml_edit::DocumentMut;
pub use crate::output::{ColorMode, OutputFormat};
use crate::{
cargo_toml_editor::CargoTomlEditor,
context::{PackageContext, WorkspaceContext},
diagnostics::ShearAnalysis,
output::Renderer,
package_processor::{PackageAnalysis, PackageProcessor, WorkspaceAnalysis},
util::read_to_string,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options("shear"), version(VERSION))]
pub struct CargoShearOptions {
#[bpaf(long)]
fix: bool,
#[bpaf(long)]
expand: bool,
#[bpaf(long("check-test-targets"))]
check_test_targets: bool,
#[bpaf(long("deny-warnings"))]
deny_warnings: 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,
check_test_targets: false,
deny_warnings: 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_check_test_targets(mut self) -> Self {
self.check_test_targets = true;
self
}
#[must_use]
pub const fn with_deny_warnings(mut self) -> Self {
self.deny_warnings = 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
}
#[must_use]
pub fn resolve(mut self) -> Self {
self.format = self.format.resolve();
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 {
let analysis = ShearAnalysis::new(options.clone());
Self { writer, options, analysis }
}
#[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);
}
self.determine_exit_code()
}
Err(err) => {
let _ = writeln!(self.writer, "error: {err:?}");
ExitCode::from(2)
}
}
}
const fn determine_exit_code(&self) -> ExitCode {
if self.options.fix && self.analysis.fixed > 0 && self.analysis.errors == 0 {
return ExitCode::SUCCESS;
}
let has_errors = self.analysis.errors > 0;
let has_warnings = self.options.deny_warnings && self.analysis.warnings > 0;
if has_errors || has_warnings { ExitCode::FAILURE } else { ExitCode::SUCCESS }
}
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)
.verbose(true)
.exec()
.map_err(|e| anyhow::anyhow!("Metadata error: {e}"))?;
let processor = PackageProcessor::new(self.options.expand, self.options.check_test_targets);
let workspace_ctx = WorkspaceContext::new(&metadata)?;
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 total = packages.len();
let results: Vec<_> = if self.options.expand {
packages
.iter()
.enumerate()
.map(|(index, package)| {
eprintln!(
"{:>12} {} [{}/{}]",
"Expanding".bright_cyan().bold(),
package.name,
index + 1,
total
);
Self::process_package(&processor, &workspace_ctx, package, &metadata)
})
.collect::<Result<Vec<_>>>()?
} else {
packages
.par_iter()
.map(|package| {
Self::process_package(&processor, &workspace_ctx, package, &metadata)
})
.collect::<Result<Vec<_>>>()?
};
let mut used_workspace_ignore_paths: FxHashSet<String> = FxHashSet::default();
let mut used_ignores: FxHashSet<String> = FxHashSet::default();
for (ctx, result) in results {
let fixed = self.fix_package_issues(&ctx.manifest_path, &result)?;
used_workspace_ignore_paths.extend(result.used_workspace_ignore_paths.iter().cloned());
used_ignores.extend(result.used_ignores.iter().cloned());
self.analysis.add_package_result(&ctx, &result, fixed);
}
if self.options.package.is_empty() && self.options.exclude.is_empty() {
let workspace_result = PackageProcessor::process_workspace(
&workspace_ctx,
&self.analysis.packages,
&used_workspace_ignore_paths,
used_ignores,
);
let fixed =
self.fix_workspace_issues(&workspace_ctx.manifest_path, &workspace_result)?;
self.analysis.add_workspace_result(&workspace_ctx, &workspace_result, fixed);
}
Ok(())
}
fn process_package<'a>(
processor: &PackageProcessor,
workspace_ctx: &'a WorkspaceContext,
package: &Package,
metadata: &'a Metadata,
) -> Result<(PackageContext<'a>, PackageAnalysis)> {
let ctx = PackageContext::new(workspace_ctx, package, metadata)?;
let result = processor.process_package(&ctx)?;
Ok((ctx, result))
}
fn fix_package_issues(&self, manifest_path: &Path, result: &PackageAnalysis) -> Result<usize> {
if !self.options.fix {
return Ok(0);
}
if !result.has_fixable_issues() {
return Ok(0);
}
let content = 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,
);
let mut flag_fixes = 0usize;
if !result.test_disabled_with_tests.is_empty() {
flag_fixes += usize::from(CargoTomlEditor::remove_lib_flag(&mut manifest, "test"));
}
if !result.test_enabled_without_tests.is_empty() {
CargoTomlEditor::set_lib_flag_false(&mut manifest, "test");
flag_fixes += 1;
}
if !result.doctest_disabled_with_doctests.is_empty() {
flag_fixes += usize::from(CargoTomlEditor::remove_lib_flag(&mut manifest, "doctest"));
}
if !result.doctest_enabled_without_doctests.is_empty() {
CargoTomlEditor::set_lib_flag_false(&mut manifest, "doctest");
flag_fixes += 1;
}
fs::write(manifest_path, manifest.to_string())?;
Ok(fixed_unused + fixed_misplaced + flag_fixes)
}
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 = 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)
}
}