// Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/). #![deny(warnings)] #![allow(clippy::match_like_matches_macro)] use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; use std::env; use std::ffi::OsStr; use std::fs; use std::hash::{Hash, Hasher}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::Command; use std::str; use clap::{AppSettings, CommandFactory, Parser}; #[path = "test/mod.rs"] #[cfg(test)] mod cargo_fmt_tests; #[derive(Parser)] #[clap( global_setting(AppSettings::NoAutoVersion), bin_name = "cargo fmt", about = "This utility formats all bin and lib files of \ the current crate using rustfmt." )] pub struct Opts { /// No output printed to stdout #[clap(short = 'q', long = "quiet")] quiet: bool, /// Use verbose output #[clap(short = 'v', long = "verbose")] verbose: bool, /// Print rustfmt version and exit #[clap(long = "version")] version: bool, /// Specify package to format #[clap( short = 'p', long = "package", value_name = "package", multiple_values = true )] packages: Vec, /// Specify path to Cargo.toml #[clap(long = "manifest-path", value_name = "manifest-path")] manifest_path: Option, /// Specify message-format: short|json|human #[clap(long = "message-format", value_name = "message-format")] message_format: Option, /// Options passed to rustfmt // 'raw = true' to make `--` explicit. #[clap(name = "rustfmt_options", raw(true))] rustfmt_options: Vec, /// Format all packages, and also their local path-based dependencies #[clap(long = "all")] format_all: bool, /// Run rustfmt in check mode #[clap(long = "check")] check: bool, } fn main() { let exit_status = execute(); std::io::stdout().flush().unwrap(); std::process::exit(exit_status); } const SUCCESS: i32 = 0; const FAILURE: i32 = 1; fn execute() -> i32 { // Drop extra `fmt` argument provided by `cargo`. let mut found_fmt = false; let args = env::args().filter(|x| { if found_fmt { true } else { found_fmt = x == "fmt"; x != "fmt" } }); let opts = Opts::parse_from(args); let verbosity = match (opts.verbose, opts.quiet) { (false, false) => Verbosity::Normal, (false, true) => Verbosity::Quiet, (true, false) => Verbosity::Verbose, (true, true) => { print_usage_to_stderr("quiet mode and verbose mode are not compatible"); return FAILURE; } }; if opts.version { return handle_command_status(get_rustfmt_info(&[String::from("--version")])); } if opts.rustfmt_options.iter().any(|s| { ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str()) || s.starts_with("--help=") || s.starts_with("--print-config=") }) { return handle_command_status(get_rustfmt_info(&opts.rustfmt_options)); } let strategy = CargoFmtStrategy::from_opts(&opts); let mut rustfmt_args = opts.rustfmt_options; if opts.check { let check_flag = "--check"; if !rustfmt_args.iter().any(|o| o == check_flag) { rustfmt_args.push(check_flag.to_owned()); } } if let Some(message_format) = opts.message_format { if let Err(msg) = convert_message_format_to_rustfmt_args(&message_format, &mut rustfmt_args) { print_usage_to_stderr(&msg); return FAILURE; } } if let Some(specified_manifest_path) = opts.manifest_path { if !specified_manifest_path.ends_with("Cargo.toml") { print_usage_to_stderr("the manifest-path must be a path to a Cargo.toml file"); return FAILURE; } let manifest_path = PathBuf::from(specified_manifest_path); handle_command_status(format_crate( verbosity, &strategy, rustfmt_args, Some(&manifest_path), )) } else { handle_command_status(format_crate(verbosity, &strategy, rustfmt_args, None)) } } fn rustfmt_command() -> Command { let rustfmt_var = env::var_os("RUSTFMT"); let rustfmt = match &rustfmt_var { Some(rustfmt) => rustfmt, None => OsStr::new("rustfmt"), }; Command::new(rustfmt) } fn convert_message_format_to_rustfmt_args( message_format: &str, rustfmt_args: &mut Vec, ) -> Result<(), String> { let mut contains_emit_mode = false; let mut contains_check = false; let mut contains_list_files = false; for arg in rustfmt_args.iter() { if arg.starts_with("--emit") { contains_emit_mode = true; } if arg == "--check" { contains_check = true; } if arg == "-l" || arg == "--files-with-diff" { contains_list_files = true; } } match message_format { "short" => { if !contains_list_files { rustfmt_args.push(String::from("-l")); } Ok(()) } "json" => { if contains_emit_mode { return Err(String::from( "cannot include --emit arg when --message-format is set to json", )); } if contains_check { return Err(String::from( "cannot include --check arg when --message-format is set to json", )); } rustfmt_args.push(String::from("--emit")); rustfmt_args.push(String::from("json")); Ok(()) } "human" => Ok(()), _ => { return Err(format!( "invalid --message-format value: {}. Allowed values are: short|json|human", message_format )); } } } fn print_usage_to_stderr(reason: &str) { eprintln!("{}", reason); let app = Opts::command(); app.after_help("") .write_help(&mut io::stderr()) .expect("failed to write to stderr"); } #[derive(Debug, Clone, Copy, PartialEq)] pub enum Verbosity { Verbose, Normal, Quiet, } fn handle_command_status(status: Result) -> i32 { match status { Err(e) => { print_usage_to_stderr(&e.to_string()); FAILURE } Ok(status) => status, } } fn get_rustfmt_info(args: &[String]) -> Result { let mut command = rustfmt_command() .stdout(std::process::Stdio::inherit()) .args(args) .spawn() .map_err(|e| match e.kind() { io::ErrorKind::NotFound => io::Error::new( io::ErrorKind::Other, "Could not run rustfmt, please make sure it is in your PATH.", ), _ => e, })?; let result = command.wait()?; if result.success() { Ok(SUCCESS) } else { Ok(result.code().unwrap_or(SUCCESS)) } } fn format_crate( verbosity: Verbosity, strategy: &CargoFmtStrategy, rustfmt_args: Vec, manifest_path: Option<&Path>, ) -> Result { let targets = get_targets(strategy, manifest_path)?; // Currently only bin and lib files get formatted. run_rustfmt(&targets, &rustfmt_args, verbosity) } /// Target uses a `path` field for equality and hashing. #[derive(Debug)] pub struct Target { /// A path to the main source file of the target. path: PathBuf, /// A kind of target (e.g., lib, bin, example, ...). kind: String, /// Rust edition for this target. edition: String, } impl Target { pub fn from_target(target: &cargo_metadata::Target) -> Self { let path = PathBuf::from(&target.src_path); let canonicalized = fs::canonicalize(&path).unwrap_or(path); Target { path: canonicalized, kind: target.kind[0].clone(), edition: target.edition.clone(), } } } impl PartialEq for Target { fn eq(&self, other: &Target) -> bool { self.path == other.path } } impl PartialOrd for Target { fn partial_cmp(&self, other: &Target) -> Option { Some(self.path.cmp(&other.path)) } } impl Ord for Target { fn cmp(&self, other: &Target) -> Ordering { self.path.cmp(&other.path) } } impl Eq for Target {} impl Hash for Target { fn hash(&self, state: &mut H) { self.path.hash(state); } } #[derive(Debug, PartialEq, Eq)] pub enum CargoFmtStrategy { /// Format every packages and dependencies. All, /// Format packages that are specified by the command line argument. Some(Vec), /// Format the root packages only. Root, } impl CargoFmtStrategy { pub fn from_opts(opts: &Opts) -> CargoFmtStrategy { match (opts.format_all, opts.packages.is_empty()) { (false, true) => CargoFmtStrategy::Root, (true, _) => CargoFmtStrategy::All, (false, false) => CargoFmtStrategy::Some(opts.packages.clone()), } } } /// Based on the specified `CargoFmtStrategy`, returns a set of main source files. fn get_targets( strategy: &CargoFmtStrategy, manifest_path: Option<&Path>, ) -> Result, io::Error> { let mut targets = BTreeSet::new(); match *strategy { CargoFmtStrategy::Root => get_targets_root_only(manifest_path, &mut targets)?, CargoFmtStrategy::All => { get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())? } CargoFmtStrategy::Some(ref hitlist) => { get_targets_with_hitlist(manifest_path, hitlist, &mut targets)? } } if targets.is_empty() { Err(io::Error::new( io::ErrorKind::Other, "Failed to find targets".to_owned(), )) } else { Ok(targets) } } fn get_targets_root_only( manifest_path: Option<&Path>, targets: &mut BTreeSet, ) -> Result<(), io::Error> { let metadata = get_cargo_metadata(manifest_path)?; let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?; let (in_workspace_root, current_dir_manifest) = if let Some(target_manifest) = manifest_path { ( workspace_root_path == target_manifest, target_manifest.canonicalize()?, ) } else { let current_dir = env::current_dir()?.canonicalize()?; ( workspace_root_path == current_dir, current_dir.join("Cargo.toml"), ) }; let package_targets = match metadata.packages.len() { 1 => metadata.packages.into_iter().next().unwrap().targets, _ => metadata .packages .into_iter() .filter(|p| { in_workspace_root || PathBuf::from(&p.manifest_path) .canonicalize() .unwrap_or_default() == current_dir_manifest }) .flat_map(|p| p.targets) .collect(), }; for target in package_targets { targets.insert(Target::from_target(&target)); } Ok(()) } fn get_targets_recursive( manifest_path: Option<&Path>, targets: &mut BTreeSet, visited: &mut BTreeSet, ) -> Result<(), io::Error> { let metadata = get_cargo_metadata(manifest_path)?; for package in &metadata.packages { add_targets(&package.targets, targets); // Look for local dependencies using information available since cargo v1.51 // It's theoretically possible someone could use a newer version of rustfmt with // a much older version of `cargo`, but we don't try to explicitly support that scenario. // If someone reports an issue with path-based deps not being formatted, be sure to // confirm their version of `cargo` (not `cargo-fmt`) is >= v1.51 // https://github.com/rust-lang/cargo/pull/8994 for dependency in &package.dependencies { if dependency.path.is_none() || visited.contains(&dependency.name) { continue; } let manifest_path = PathBuf::from(dependency.path.as_ref().unwrap()).join("Cargo.toml"); if manifest_path.exists() && !metadata .packages .iter() .any(|p| p.manifest_path.eq(&manifest_path)) { visited.insert(dependency.name.to_owned()); get_targets_recursive(Some(&manifest_path), targets, visited)?; } } } Ok(()) } fn get_targets_with_hitlist( manifest_path: Option<&Path>, hitlist: &[String], targets: &mut BTreeSet, ) -> Result<(), io::Error> { let metadata = get_cargo_metadata(manifest_path)?; let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist); for package in metadata.packages { if workspace_hitlist.remove(&package.name) { for target in package.targets { targets.insert(Target::from_target(&target)); } } } if workspace_hitlist.is_empty() { Ok(()) } else { let package = workspace_hitlist.iter().next().unwrap(); Err(io::Error::new( io::ErrorKind::InvalidInput, format!("package `{}` is not a member of the workspace", package), )) } } fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet) { for target in target_paths { targets.insert(Target::from_target(target)); } } fn run_rustfmt( targets: &BTreeSet, fmt_args: &[String], verbosity: Verbosity, ) -> Result { let by_edition = targets .iter() .inspect(|t| { if verbosity == Verbosity::Verbose { println!("[{} ({})] {:?}", t.kind, t.edition, t.path) } }) .fold(BTreeMap::new(), |mut h, t| { h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path); h }); let mut status = vec![]; for (edition, files) in by_edition { let stdout = if verbosity == Verbosity::Quiet { std::process::Stdio::null() } else { std::process::Stdio::inherit() }; if verbosity == Verbosity::Verbose { print!("rustfmt"); print!(" --edition {}", edition); fmt_args.iter().for_each(|f| print!(" {}", f)); files.iter().for_each(|f| print!(" {}", f.display())); println!(); } let mut command = rustfmt_command() .stdout(stdout) .args(files) .args(&["--edition", edition]) .args(fmt_args) .spawn() .map_err(|e| match e.kind() { io::ErrorKind::NotFound => io::Error::new( io::ErrorKind::Other, "Could not run rustfmt, please make sure it is in your PATH.", ), _ => e, })?; status.push(command.wait()?); } Ok(status .iter() .filter_map(|s| if s.success() { None } else { s.code() }) .next() .unwrap_or(SUCCESS)) } fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result { let mut cmd = cargo_metadata::MetadataCommand::new(); cmd.no_deps(); if let Some(manifest_path) = manifest_path { cmd.manifest_path(manifest_path); } cmd.other_options(vec![String::from("--offline")]); match cmd.exec() { Ok(metadata) => Ok(metadata), Err(_) => { cmd.other_options(vec![]); match cmd.exec() { Ok(metadata) => Ok(metadata), Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())), } } } }