1 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/).
4 #![allow(clippy::match_like_matches_macro)]
6 use std::cmp::Ordering;
7 use std::collections::{BTreeMap, BTreeSet};
11 use std::hash::{Hash, Hasher};
12 use std::io::{self, Write};
13 use std::iter::FromIterator;
14 use std::path::{Path, PathBuf};
15 use std::process::Command;
18 use structopt::StructOpt;
20 #[derive(StructOpt, Debug)]
22 bin_name = "cargo fmt",
23 about = "This utility formats all bin and lib files of \
24 the current crate using rustfmt."
27 /// No output printed to stdout
28 #[structopt(short = "q", long = "quiet")]
31 /// Use verbose output
32 #[structopt(short = "v", long = "verbose")]
35 /// Print rustfmt version and exit
36 #[structopt(long = "version")]
39 /// Specify package to format (only usable in workspaces)
40 #[structopt(short = "p", long = "package", value_name = "package")]
41 packages: Vec<String>,
43 /// Specify path to Cargo.toml
44 #[structopt(long = "manifest-path", value_name = "manifest-path")]
45 manifest_path: Option<String>,
47 /// Specify message-format: short|json|human
48 #[structopt(long = "message-format", value_name = "message-format")]
49 message_format: Option<String>,
51 /// Options passed to rustfmt
52 // 'raw = true' to make `--` explicit.
53 #[structopt(name = "rustfmt_options", raw(true))]
54 rustfmt_options: Vec<String>,
56 /// Format all packages (only usable in workspaces)
57 #[structopt(long = "all")]
62 let exit_status = execute();
63 std::io::stdout().flush().unwrap();
64 std::process::exit(exit_status);
67 const SUCCESS: i32 = 0;
68 const FAILURE: i32 = 1;
71 // Drop extra `fmt` argument provided by `cargo`.
72 let mut found_fmt = false;
73 let args = env::args().filter(|x| {
77 found_fmt = x == "fmt";
82 let opts = Opts::from_iter(args);
84 let verbosity = match (opts.verbose, opts.quiet) {
85 (false, false) => Verbosity::Normal,
86 (false, true) => Verbosity::Quiet,
87 (true, false) => Verbosity::Verbose,
89 print_usage_to_stderr("quiet mode and verbose mode are not compatible");
95 return handle_command_status(get_rustfmt_info(&[String::from("--version")]));
97 if opts.rustfmt_options.iter().any(|s| {
98 ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str())
99 || s.starts_with("--help=")
100 || s.starts_with("--print-config=")
102 return handle_command_status(get_rustfmt_info(&opts.rustfmt_options));
105 let strategy = CargoFmtStrategy::from_opts(&opts);
106 let mut rustfmt_args = opts.rustfmt_options;
107 if let Some(message_format) = opts.message_format {
108 if let Err(msg) = convert_message_format_to_rustfmt_args(&message_format, &mut rustfmt_args)
110 print_usage_to_stderr(&msg);
115 if let Some(specified_manifest_path) = opts.manifest_path {
116 if !specified_manifest_path.ends_with("Cargo.toml") {
117 print_usage_to_stderr("the manifest-path must be a path to a Cargo.toml file");
120 let manifest_path = PathBuf::from(specified_manifest_path);
121 handle_command_status(format_crate(
125 Some(&manifest_path),
128 handle_command_status(format_crate(verbosity, &strategy, rustfmt_args, None))
132 fn rustfmt_command() -> Command {
133 let rustfmt_var = env::var_os("RUSTFMT");
134 let rustfmt = match &rustfmt_var {
135 Some(rustfmt) => rustfmt,
136 None => OsStr::new("rustfmt"),
138 Command::new(rustfmt)
141 fn convert_message_format_to_rustfmt_args(
142 message_format: &str,
143 rustfmt_args: &mut Vec<String>,
144 ) -> Result<(), String> {
145 let mut contains_emit_mode = false;
146 let mut contains_check = false;
147 let mut contains_list_files = false;
148 for arg in rustfmt_args.iter() {
149 if arg.starts_with("--emit") {
150 contains_emit_mode = true;
152 if arg == "--check" {
153 contains_check = true;
155 if arg == "-l" || arg == "--files-with-diff" {
156 contains_list_files = true;
159 match message_format {
161 if !contains_list_files {
162 rustfmt_args.push(String::from("-l"));
167 if contains_emit_mode {
168 return Err(String::from(
169 "cannot include --emit arg when --message-format is set to json",
173 return Err(String::from(
174 "cannot include --check arg when --message-format is set to json",
177 rustfmt_args.push(String::from("--emit"));
178 rustfmt_args.push(String::from("json"));
184 "invalid --message-format value: {}. Allowed values are: short|json|human",
191 fn print_usage_to_stderr(reason: &str) {
192 eprintln!("{}", reason);
193 let app = Opts::clap();
195 .write_help(&mut io::stderr())
196 .expect("failed to write to stderr");
199 #[derive(Debug, Clone, Copy, PartialEq)]
206 fn handle_command_status(status: Result<i32, io::Error>) -> i32 {
209 print_usage_to_stderr(&e.to_string());
212 Ok(status) => status,
216 fn get_rustfmt_info(args: &[String]) -> Result<i32, io::Error> {
217 let mut command = rustfmt_command()
218 .stdout(std::process::Stdio::inherit())
221 .map_err(|e| match e.kind() {
222 io::ErrorKind::NotFound => io::Error::new(
223 io::ErrorKind::Other,
224 "Could not run rustfmt, please make sure it is in your PATH.",
228 let result = command.wait()?;
229 if result.success() {
232 Ok(result.code().unwrap_or(SUCCESS))
237 verbosity: Verbosity,
238 strategy: &CargoFmtStrategy,
239 rustfmt_args: Vec<String>,
240 manifest_path: Option<&Path>,
241 ) -> Result<i32, io::Error> {
242 let targets = get_targets(strategy, manifest_path)?;
244 // Currently only bin and lib files get formatted.
245 run_rustfmt(&targets, &rustfmt_args, verbosity)
248 /// Target uses a `path` field for equality and hashing.
251 /// A path to the main source file of the target.
253 /// A kind of target (e.g., lib, bin, example, ...).
255 /// Rust edition for this target.
260 pub fn from_target(target: &cargo_metadata::Target) -> Self {
261 let path = PathBuf::from(&target.src_path);
262 let canonicalized = fs::canonicalize(&path).unwrap_or(path);
266 kind: target.kind[0].clone(),
267 edition: target.edition.clone(),
272 impl PartialEq for Target {
273 fn eq(&self, other: &Target) -> bool {
274 self.path == other.path
278 impl PartialOrd for Target {
279 fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
280 Some(self.path.cmp(&other.path))
284 impl Ord for Target {
285 fn cmp(&self, other: &Target) -> Ordering {
286 self.path.cmp(&other.path)
290 impl Eq for Target {}
292 impl Hash for Target {
293 fn hash<H: Hasher>(&self, state: &mut H) {
294 self.path.hash(state);
298 #[derive(Debug, PartialEq, Eq)]
299 pub enum CargoFmtStrategy {
300 /// Format every packages and dependencies.
302 /// Format packages that are specified by the command line argument.
304 /// Format the root packages only.
308 impl CargoFmtStrategy {
309 pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
310 match (opts.format_all, opts.packages.is_empty()) {
311 (false, true) => CargoFmtStrategy::Root,
312 (true, _) => CargoFmtStrategy::All,
313 (false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
318 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
320 strategy: &CargoFmtStrategy,
321 manifest_path: Option<&Path>,
322 ) -> Result<BTreeSet<Target>, io::Error> {
323 let mut targets = BTreeSet::new();
326 CargoFmtStrategy::Root => get_targets_root_only(manifest_path, &mut targets)?,
327 CargoFmtStrategy::All => {
328 get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())?
330 CargoFmtStrategy::Some(ref hitlist) => {
331 get_targets_with_hitlist(manifest_path, hitlist, &mut targets)?
335 if targets.is_empty() {
337 io::ErrorKind::Other,
338 "Failed to find targets".to_owned(),
345 fn get_targets_root_only(
346 manifest_path: Option<&Path>,
347 targets: &mut BTreeSet<Target>,
348 ) -> Result<(), io::Error> {
349 let metadata = get_cargo_metadata(manifest_path, false)?;
350 let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
351 let (in_workspace_root, current_dir_manifest) = if let Some(target_manifest) = manifest_path {
353 workspace_root_path == target_manifest,
354 target_manifest.canonicalize()?,
357 let current_dir = env::current_dir()?.canonicalize()?;
359 workspace_root_path == current_dir,
360 current_dir.join("Cargo.toml"),
364 let package_targets = match metadata.packages.len() {
365 1 => metadata.packages.into_iter().next().unwrap().targets,
371 || PathBuf::from(&p.manifest_path)
374 == current_dir_manifest
381 for target in package_targets {
382 targets.insert(Target::from_target(&target));
388 fn get_targets_recursive(
389 manifest_path: Option<&Path>,
390 mut targets: &mut BTreeSet<Target>,
391 visited: &mut BTreeSet<String>,
392 ) -> Result<(), io::Error> {
393 let metadata = get_cargo_metadata(manifest_path, false)?;
394 let metadata_with_deps = get_cargo_metadata(manifest_path, true)?;
396 for package in metadata.packages {
397 add_targets(&package.targets, &mut targets);
399 // Look for local dependencies.
400 for dependency in package.dependencies {
401 if dependency.source.is_some() || visited.contains(&dependency.name) {
405 let dependency_package = metadata_with_deps
408 .find(|p| p.name == dependency.name && p.source.is_none());
409 let manifest_path = if let Some(dep_pkg) = dependency_package {
410 PathBuf::from(&dep_pkg.manifest_path)
412 let mut package_manifest_path = PathBuf::from(&package.manifest_path);
413 package_manifest_path.pop();
414 package_manifest_path.push(&dependency.name);
415 package_manifest_path.push("Cargo.toml");
416 package_manifest_path
419 if manifest_path.exists() {
420 visited.insert(dependency.name);
421 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
429 fn get_targets_with_hitlist(
430 manifest_path: Option<&Path>,
432 targets: &mut BTreeSet<Target>,
433 ) -> Result<(), io::Error> {
434 let metadata = get_cargo_metadata(manifest_path, false)?;
436 let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
438 for package in metadata.packages {
439 if workspace_hitlist.remove(&package.name) {
440 for target in package.targets {
441 targets.insert(Target::from_target(&target));
446 if workspace_hitlist.is_empty() {
449 let package = workspace_hitlist.iter().next().unwrap();
451 io::ErrorKind::InvalidInput,
452 format!("package `{}` is not a member of the workspace", package),
457 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
458 for target in target_paths {
459 targets.insert(Target::from_target(target));
464 targets: &BTreeSet<Target>,
466 verbosity: Verbosity,
467 ) -> Result<i32, io::Error> {
468 let by_edition = targets
471 if verbosity == Verbosity::Verbose {
472 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
475 .fold(BTreeMap::new(), |mut h, t| {
476 h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
480 let mut status = vec![];
481 for (edition, files) in by_edition {
482 let stdout = if verbosity == Verbosity::Quiet {
483 std::process::Stdio::null()
485 std::process::Stdio::inherit()
488 if verbosity == Verbosity::Verbose {
490 print!(" --edition {}", edition);
491 fmt_args.iter().for_each(|f| print!(" {}", f));
492 files.iter().for_each(|f| print!(" {}", f.display()));
496 let mut command = rustfmt_command()
499 .args(&["--edition", edition])
502 .map_err(|e| match e.kind() {
503 io::ErrorKind::NotFound => io::Error::new(
504 io::ErrorKind::Other,
505 "Could not run rustfmt, please make sure it is in your PATH.",
510 status.push(command.wait()?);
515 .filter_map(|s| if s.success() { None } else { s.code() })
520 fn get_cargo_metadata(
521 manifest_path: Option<&Path>,
523 ) -> Result<cargo_metadata::Metadata, io::Error> {
524 let mut cmd = cargo_metadata::MetadataCommand::new();
528 if let Some(manifest_path) = manifest_path {
529 cmd.manifest_path(manifest_path);
531 cmd.other_options(&[String::from("--offline")]);
534 Ok(metadata) => Ok(metadata),
536 cmd.other_options(vec![]);
538 Ok(metadata) => Ok(metadata),
539 Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
546 mod cargo_fmt_tests {
550 fn default_options() {
551 let empty: Vec<String> = vec![];
552 let o = Opts::from_iter(&empty);
553 assert_eq!(false, o.quiet);
554 assert_eq!(false, o.verbose);
555 assert_eq!(false, o.version);
556 assert_eq!(empty, o.packages);
557 assert_eq!(empty, o.rustfmt_options);
558 assert_eq!(false, o.format_all);
559 assert_eq!(None, o.manifest_path);
560 assert_eq!(None, o.message_format);
565 let o = Opts::from_iter(&[
578 assert_eq!(true, o.quiet);
579 assert_eq!(false, o.verbose);
580 assert_eq!(false, o.version);
581 assert_eq!(vec!["p1", "p2"], o.packages);
582 assert_eq!(vec!["--edition", "2018"], o.rustfmt_options);
583 assert_eq!(false, o.format_all);
584 assert_eq!(Some(String::from("short")), o.message_format);
588 fn unexpected_option() {
591 .get_matches_from_safe(&["test", "unexpected"])
597 fn unexpected_flag() {
600 .get_matches_from_safe(&["test", "--flag"])
606 fn mandatory_separator() {
609 .get_matches_from_safe(&["test", "--check"])
614 .get_matches_from_safe(&["test", "--", "--check"])
620 fn multiple_packages_one_by_one() {
621 let o = Opts::from_iter(&[
630 assert_eq!(3, o.packages.len());
634 fn multiple_packages_grouped() {
635 let o = Opts::from_iter(&[
644 assert_eq!(4, o.packages.len());
648 fn empty_packages_1() {
649 assert!(Opts::clap().get_matches_from_safe(&["test", "-p"]).is_err());
653 fn empty_packages_2() {
656 .get_matches_from_safe(&["test", "-p", "--", "--check"])
662 fn empty_packages_3() {
665 .get_matches_from_safe(&["test", "-p", "--verbose"])
671 fn empty_packages_4() {
674 .get_matches_from_safe(&["test", "-p", "--check"])
679 mod convert_message_format_to_rustfmt_args_tests {
683 fn invalid_message_format() {
685 convert_message_format_to_rustfmt_args("awesome", &mut vec![]),
687 "invalid --message-format value: awesome. Allowed values are: short|json|human"
693 fn json_message_format_and_check_arg() {
694 let mut args = vec![String::from("--check")];
696 convert_message_format_to_rustfmt_args("json", &mut args),
698 "cannot include --check arg when --message-format is set to json"
704 fn json_message_format_and_emit_arg() {
705 let mut args = vec![String::from("--emit"), String::from("checkstyle")];
707 convert_message_format_to_rustfmt_args("json", &mut args),
709 "cannot include --emit arg when --message-format is set to json"
715 fn json_message_format() {
716 let mut args = vec![String::from("--edition"), String::from("2018")];
717 assert!(convert_message_format_to_rustfmt_args("json", &mut args).is_ok());
721 String::from("--edition"),
722 String::from("2018"),
723 String::from("--emit"),
730 fn human_message_format() {
731 let exp_args = vec![String::from("--emit"), String::from("json")];
732 let mut act_args = exp_args.clone();
733 assert!(convert_message_format_to_rustfmt_args("human", &mut act_args).is_ok());
734 assert_eq!(act_args, exp_args);
738 fn short_message_format() {
739 let mut args = vec![String::from("--check")];
740 assert!(convert_message_format_to_rustfmt_args("short", &mut args).is_ok());
741 assert_eq!(args, vec![String::from("--check"), String::from("-l")]);
745 fn short_message_format_included_short_list_files_flag() {
746 let mut args = vec![String::from("--check"), String::from("-l")];
747 assert!(convert_message_format_to_rustfmt_args("short", &mut args).is_ok());
748 assert_eq!(args, vec![String::from("--check"), String::from("-l")]);
752 fn short_message_format_included_long_list_files_flag() {
753 let mut args = vec![String::from("--check"), String::from("--files-with-diff")];
754 assert!(convert_message_format_to_rustfmt_args("short", &mut args).is_ok());
757 vec![String::from("--check"), String::from("--files-with-diff")]