mod search_unused;
use crate::search_unused::find_unused;
use anyhow::Context;
use rayon::prelude::*;
use std::path::Path;
use std::str::FromStr;
use std::{fs, path::PathBuf};
use walkdir::WalkDir;
#[derive(Clone, Copy)]
pub(crate) enum UseCargoMetadata {
Yes,
No,
}
#[cfg(test)]
impl UseCargoMetadata {
fn all() -> &'static [UseCargoMetadata] {
&[UseCargoMetadata::Yes, UseCargoMetadata::No]
}
}
impl From<UseCargoMetadata> for bool {
fn from(v: UseCargoMetadata) -> bool {
matches!(v, UseCargoMetadata::Yes)
}
}
impl From<bool> for UseCargoMetadata {
fn from(b: bool) -> Self {
if b {
Self::Yes
} else {
Self::No
}
}
}
#[derive(argh::FromArgs)]
#[argh(description = r#"
cargo-machete: Helps find unused dependencies in a fast yet imprecise way.
Exit code:
0: when no unused dependencies are found
1: when at least one unused (non-ignored) dependency is found
2: on error
"#)]
struct MacheteArgs {
#[argh(switch)]
with_metadata: bool,
#[argh(switch)]
skip_target_dir: bool,
#[argh(switch)]
fix: bool,
#[argh(switch)]
version: bool,
#[argh(positional, greedy)]
paths: Vec<PathBuf>,
}
fn collect_paths(path: &Path, skip_target_dir: bool) -> Result<Vec<PathBuf>, walkdir::Error> {
let walker = WalkDir::new(path).into_iter();
let manifest_path_entries = if skip_target_dir {
walker
.filter_entry(|entry| !entry.path().ends_with("target"))
.collect()
} else {
walker.collect::<Vec<_>>()
};
manifest_path_entries
.into_iter()
.filter(|entry| match entry {
Ok(entry) => entry.file_name() == "Cargo.toml",
Err(_) => true,
})
.map(|res_entry| res_entry.map(|e| e.into_path()))
.collect()
}
fn running_as_cargo_cmd() -> bool {
std::env::var("CARGO").is_ok() && std::env::var("CARGO_PKG_NAME").is_err()
}
fn run_machete() -> anyhow::Result<bool> {
pretty_env_logger::init();
let mut args: MacheteArgs = if running_as_cargo_cmd() {
argh::cargo_from_env()
} else {
argh::from_env()
};
if args.version {
println!("{}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
if args.paths.is_empty() {
eprintln!("Analyzing dependencies of crates in this directory...");
args.paths.push(std::env::current_dir()?);
} else {
eprintln!(
"Analyzing dependencies of crates in {}...",
args.paths
.iter()
.cloned()
.map(|path| path.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(",")
);
}
let mut has_unused_dependencies = false;
let mut walkdir_errors = Vec::new();
for path in args.paths {
let manifest_path_entries = match collect_paths(&path, args.skip_target_dir) {
Ok(entries) => entries,
Err(err) => {
walkdir_errors.push(err);
continue;
}
};
let results = manifest_path_entries
.par_iter()
.filter_map(|manifest_path| {
match find_unused(manifest_path, args.with_metadata.into()) {
Ok(Some(analysis)) => {
if analysis.unused.is_empty() {
None
} else {
Some((analysis, manifest_path))
}
}
Ok(None) => {
log::info!(
"{} is a virtual manifest for a workspace",
manifest_path.to_string_lossy()
);
None
}
Err(err) => {
eprintln!("error when handling {}: {}", manifest_path.display(), err);
None
}
}
})
.collect::<Vec<_>>();
if results.is_empty() {
println!(
"cargo-machete didn't find any unused dependencies in {}. Good job!",
path.to_string_lossy()
);
continue;
}
println!(
"cargo-machete found the following unused dependencies in {}:",
path.to_string_lossy()
);
for (analysis, path) in results {
println!("{} -- {}:", analysis.package_name, path.to_string_lossy());
for dep in &analysis.unused {
println!("\t{dep}");
has_unused_dependencies = true; }
for dep in &analysis.ignored_used {
eprintln!("\t⚠️ {dep} was marked as ignored, but is actually used!");
}
if args.fix {
let fixed = remove_dependencies(&fs::read_to_string(path)?, &analysis.unused)?;
fs::write(path, fixed).expect("Cargo.toml write error");
}
}
}
if has_unused_dependencies {
println!(
"\n\
If you believe cargo-machete has detected an unused dependency incorrectly,\n\
you can add the dependency to the list of dependencies to ignore in the\n\
`[package.metadata.cargo-machete]` section of the appropriate Cargo.toml.\n\
For example:\n\
\n\
[package.metadata.cargo-machete]\n\
ignored = [\"prost\"]"
);
if !args.with_metadata {
println!(
"\n\
You can also try running it with the `--with-metadata` flag for better accuracy,\n\
though this may modify your Cargo.lock files."
);
}
println!()
}
eprintln!("Done!");
if !walkdir_errors.is_empty() {
anyhow::bail!(
"Errors when walking over directories:\n{}",
walkdir_errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
);
}
Ok(has_unused_dependencies)
}
fn remove_dependencies(manifest: &str, dependencies_list: &[String]) -> anyhow::Result<String> {
let mut manifest = toml_edit::Document::from_str(manifest)?;
let dependencies = manifest
.iter_mut()
.find_map(|(k, v)| (v.is_table_like() && k == "dependencies").then_some(Some(v)))
.flatten()
.context("dependencies table is missing or empty")?
.as_table_mut()
.context("unexpected missing table, please report with a test case on https://github.com/bnjbvr/cargo-machete")?;
for k in dependencies_list {
dependencies
.remove(k)
.with_context(|| format!("Dependency {k} not found"))?;
}
let serialized = manifest.to_string();
Ok(serialized)
}
fn main() {
let exit_code = match run_machete() {
Ok(false) => 0,
Ok(true) => 1,
Err(err) => {
eprintln!("Error: {err}");
2
}
};
std::process::exit(exit_code);
}
#[cfg(test)]
const TOP_LEVEL: &str = concat!(env!("CARGO_MANIFEST_DIR"));
#[test]
fn test_ignore_target() {
let entries = collect_paths(
&PathBuf::from(TOP_LEVEL).join("./integration-tests/with-target/"),
true,
);
assert!(entries.unwrap().is_empty());
let entries = collect_paths(
&PathBuf::from(TOP_LEVEL).join("./integration-tests/with-target/"),
false,
);
assert!(!entries.unwrap().is_empty());
}