Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ log = "0.4.29"
pretty_env_logger = "0.5.0"
rayon = "1.5.2"
serde = "1.0.228"
serde_json = "1.0"
toml_edit = "0.24.0"
walkdir = "2.3.2"

Expand Down
125 changes: 87 additions & 38 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod search_unused;
use crate::search_unused::find_unused;
use anyhow::{Context, bail};
use rayon::prelude::*;
use serde::Serialize;
use std::path::Path;
use std::str::FromStr;
use std::{borrow::Cow, fs, path::PathBuf};
Expand Down Expand Up @@ -54,6 +55,10 @@ struct MacheteArgs {
#[argh(switch)]
version: bool,

/// output results as JSON for tooling integration.
#[argh(switch)]
json: bool,

/// paths to directories that must be scanned.
#[argh(positional, greedy)]
paths: Vec<PathBuf>,
Expand Down Expand Up @@ -115,6 +120,26 @@ fn running_as_cargo_cmd() -> bool {
std::env::var("CARGO").is_ok() && std::env::var("CARGO_PKG_NAME").is_err()
}

/// JSON output structure for unused dependencies.
#[derive(Serialize)]
struct JsonOutput {
/// List of crates with unused dependencies.
crates: Vec<CrateUnusedDeps>,
}

/// JSON structure for a single crate's unused dependencies.
#[derive(Serialize)]
struct CrateUnusedDeps {
/// The name of the package.
package_name: String,
/// Path to the Cargo.toml file.
manifest_path: String,
/// List of unused dependency names.
unused: Vec<String>,
/// List of dependencies marked as ignored but actually used.
ignored_used: Vec<String>,
}

/// Runs `cargo-machete`.
/// Returns Ok with a bool whether any unused dependencies were found, or Err on errors.
fn run_machete() -> anyhow::Result<bool> {
Expand All @@ -132,9 +157,11 @@ fn run_machete() -> anyhow::Result<bool> {
}

if args.paths.is_empty() {
eprintln!("Analyzing dependencies of crates in this directory...");
if !args.json {
eprintln!("Analyzing dependencies of crates in this directory...");
}
args.paths.push(PathBuf::from("."));
} else {
} else if !args.json {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these if !args.json required? The fact the tool is outputting with eprintln means this is going to stderr, and a user emitting JSON would likely be fine piping from stdout, so I think we could keep the stderr output as is. Thoughts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and made this change (and have pushed the commits), however I think we want to go back to something like I had originally. Look at: https://doc.rust-lang.org/std/macro.eprintln.html

Use eprintln! only for error and progress messages. Use println! instead for the primary output of your program.

When not in --json mode, the primary output is what it used to print, and so that should use println!. So even

eprintln!("Analyzing dependencies of crates in this directory...");

Should change to use just println! according to that.

Now, if we do that, we cannot output both JSON and plain text, so I think I'll need to make a change to make it either or.

  1. If --json flag is on, then it will only print JSON to stdout (using println!).
  2. Else, then it will only print the plain text it previously printed to stdout (using println!).

I will wait to hear back before I make this change though.

eprintln!(
"Analyzing dependencies of crates in {}...",
args.paths
Expand All @@ -147,8 +174,9 @@ fn run_machete() -> anyhow::Result<bool> {

let mut has_unused_dependencies = false;
let mut walkdir_errors = Vec::new();
let mut json_output = JsonOutput { crates: Vec::new() };

for path in args.paths {
for path in args.paths.clone() {
let manifest_path_entries = match collect_paths(
&path,
CollectPathOptions {
Expand Down Expand Up @@ -206,55 +234,76 @@ fn run_machete() -> anyhow::Result<bool> {
pathstr => pathstr,
};

if results.is_empty() {
println!("cargo-machete didn't find any unused dependencies in {location}. Good job!");
continue;
}

println!("cargo-machete found the following unused dependencies in {location}:");
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; // any unused dependency is enough to set flag to true
if args.json {
// Collect results for JSON output.
for (analysis, path) in results {
if !analysis.unused.is_empty() {
has_unused_dependencies = true;
}
json_output.crates.push(CrateUnusedDeps {
package_name: analysis.package_name.clone(),
manifest_path: path.to_string_lossy().to_string(),
unused: analysis.unused.clone(),
ignored_used: analysis.ignored_used.clone(),
});
}

for dep in &analysis.ignored_used {
eprintln!("\t⚠️ {dep} was marked as ignored, but is actually used!");
} else {
if results.is_empty() {
println!(
"cargo-machete didn't find any unused dependencies in {location}. Good job!"
);
continue;
}

if args.fix {
let fixed = remove_dependencies(&fs::read_to_string(path)?, &analysis.unused)?;
fs::write(path, fixed).expect("Cargo.toml write error");
println!("cargo-machete found the following unused dependencies in {location}:");
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; // any unused dependency is enough to set flag to 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 {
if args.json {
println!("{}", serde_json::to_string(&json_output)?);
} else {
if has_unused_dependencies {
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."
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!();
}

println!();
eprintln!("Done!");
}

eprintln!("Done!");

if !walkdir_errors.is_empty() {
anyhow::bail!(
"Errors when walking over directories:\n{}",
Expand Down