]> git.lizzy.rs Git - rust.git/blob - src/cargo-fmt/main.rs
fix: don't force a newline after an empty where clause
[rust.git] / src / cargo-fmt / main.rs
1 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/).
2
3 #![deny(warnings)]
4
5 use cargo_metadata;
6
7 use std::cmp::Ordering;
8 use std::collections::{BTreeMap, BTreeSet};
9 use std::env;
10 use std::ffi::OsStr;
11 use std::fs;
12 use std::hash::{Hash, Hasher};
13 use std::io::{self, Write};
14 use std::iter::FromIterator;
15 use std::path::{Path, PathBuf};
16 use std::process::Command;
17 use std::str;
18
19 use structopt::StructOpt;
20
21 #[derive(StructOpt, Debug)]
22 #[structopt(
23     bin_name = "cargo fmt",
24     about = "This utility formats all bin and lib files of \
25              the current crate using rustfmt."
26 )]
27 pub struct Opts {
28     /// No output printed to stdout
29     #[structopt(short = "q", long = "quiet")]
30     quiet: bool,
31
32     /// Use verbose output
33     #[structopt(short = "v", long = "verbose")]
34     verbose: bool,
35
36     /// Print rustfmt version and exit
37     #[structopt(long = "version")]
38     version: bool,
39
40     /// Specify package to format (only usable in workspaces)
41     #[structopt(short = "p", long = "package", value_name = "package")]
42     packages: Vec<String>,
43
44     /// Specify path to Cargo.toml
45     #[structopt(long = "manifest-path", value_name = "manifest-path")]
46     manifest_path: Option<String>,
47
48     /// Specify message-format: short|json|human
49     #[structopt(long = "message-format", value_name = "message-format")]
50     message_format: Option<String>,
51
52     /// Options passed to rustfmt
53     // 'raw = true' to make `--` explicit.
54     #[structopt(name = "rustfmt_options", raw(true))]
55     rustfmt_options: Vec<String>,
56
57     /// Format all packages (only usable in workspaces)
58     #[structopt(long = "all")]
59     format_all: bool,
60 }
61
62 fn main() {
63     let exit_status = execute();
64     std::io::stdout().flush().unwrap();
65     std::process::exit(exit_status);
66 }
67
68 const SUCCESS: i32 = 0;
69 const FAILURE: i32 = 1;
70
71 fn execute() -> i32 {
72     // Drop extra `fmt` argument provided by `cargo`.
73     let mut found_fmt = false;
74     let args = env::args().filter(|x| {
75         if found_fmt {
76             true
77         } else {
78             found_fmt = x == "fmt";
79             x != "fmt"
80         }
81     });
82
83     let opts = Opts::from_iter(args);
84
85     let verbosity = match (opts.verbose, opts.quiet) {
86         (false, false) => Verbosity::Normal,
87         (false, true) => Verbosity::Quiet,
88         (true, false) => Verbosity::Verbose,
89         (true, true) => {
90             print_usage_to_stderr("quiet mode and verbose mode are not compatible");
91             return FAILURE;
92         }
93     };
94
95     if opts.version {
96         return handle_command_status(get_rustfmt_info(&[String::from("--version")]));
97     }
98     if opts.rustfmt_options.iter().any(|s| {
99         ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str())
100             || s.starts_with("--help=")
101             || s.starts_with("--print-config=")
102     }) {
103         return handle_command_status(get_rustfmt_info(&opts.rustfmt_options));
104     }
105
106     let strategy = CargoFmtStrategy::from_opts(&opts);
107     let mut rustfmt_args = opts.rustfmt_options;
108     if let Some(message_format) = opts.message_format {
109         if let Err(msg) = convert_message_format_to_rustfmt_args(&message_format, &mut rustfmt_args)
110         {
111             print_usage_to_stderr(&msg);
112             return FAILURE;
113         }
114     }
115
116     if let Some(specified_manifest_path) = opts.manifest_path {
117         if !specified_manifest_path.ends_with("Cargo.toml") {
118             print_usage_to_stderr("the manifest-path must be a path to a Cargo.toml file");
119             return FAILURE;
120         }
121         let manifest_path = PathBuf::from(specified_manifest_path);
122         handle_command_status(format_crate(
123             verbosity,
124             &strategy,
125             rustfmt_args,
126             Some(&manifest_path),
127         ))
128     } else {
129         handle_command_status(format_crate(verbosity, &strategy, rustfmt_args, None))
130     }
131 }
132
133 fn rustfmt_command() -> Command {
134     let rustfmt_var = env::var_os("RUSTFMT");
135     let rustfmt = match &rustfmt_var {
136         Some(rustfmt) => rustfmt,
137         None => OsStr::new("rustfmt"),
138     };
139     Command::new(rustfmt)
140 }
141
142 fn convert_message_format_to_rustfmt_args(
143     message_format: &str,
144     rustfmt_args: &mut Vec<String>,
145 ) -> Result<(), String> {
146     let mut contains_emit_mode = false;
147     let mut contains_check = false;
148     let mut contains_list_files = false;
149     for arg in rustfmt_args.iter() {
150         if arg.starts_with("--emit") {
151             contains_emit_mode = true;
152         }
153         if arg == "--check" {
154             contains_check = true;
155         }
156         if arg == "-l" || arg == "--files-with-diff" {
157             contains_list_files = true;
158         }
159     }
160     match message_format {
161         "short" => {
162             if !contains_list_files {
163                 rustfmt_args.push(String::from("-l"));
164             }
165             Ok(())
166         }
167         "json" => {
168             if contains_emit_mode {
169                 return Err(String::from(
170                     "cannot include --emit arg when --message-format is set to json",
171                 ));
172             }
173             if contains_check {
174                 return Err(String::from(
175                     "cannot include --check arg when --message-format is set to json",
176                 ));
177             }
178             rustfmt_args.push(String::from("--emit"));
179             rustfmt_args.push(String::from("json"));
180             Ok(())
181         }
182         "human" => Ok(()),
183         _ => {
184             return Err(format!(
185                 "invalid --message-format value: {}. Allowed values are: short|json|human",
186                 message_format
187             ));
188         }
189     }
190 }
191
192 fn print_usage_to_stderr(reason: &str) {
193     eprintln!("{}", reason);
194     let app = Opts::clap();
195     app.after_help("")
196         .write_help(&mut io::stderr())
197         .expect("failed to write to stderr");
198 }
199
200 #[derive(Debug, Clone, Copy, PartialEq)]
201 pub enum Verbosity {
202     Verbose,
203     Normal,
204     Quiet,
205 }
206
207 fn handle_command_status(status: Result<i32, io::Error>) -> i32 {
208     match status {
209         Err(e) => {
210             print_usage_to_stderr(&e.to_string());
211             FAILURE
212         }
213         Ok(status) => status,
214     }
215 }
216
217 fn get_rustfmt_info(args: &[String]) -> Result<i32, io::Error> {
218     let mut command = rustfmt_command()
219         .stdout(std::process::Stdio::inherit())
220         .args(args)
221         .spawn()
222         .map_err(|e| match e.kind() {
223             io::ErrorKind::NotFound => io::Error::new(
224                 io::ErrorKind::Other,
225                 "Could not run rustfmt, please make sure it is in your PATH.",
226             ),
227             _ => e,
228         })?;
229     let result = command.wait()?;
230     if result.success() {
231         Ok(SUCCESS)
232     } else {
233         Ok(result.code().unwrap_or(SUCCESS))
234     }
235 }
236
237 fn format_crate(
238     verbosity: Verbosity,
239     strategy: &CargoFmtStrategy,
240     rustfmt_args: Vec<String>,
241     manifest_path: Option<&Path>,
242 ) -> Result<i32, io::Error> {
243     let targets = get_targets(strategy, manifest_path)?;
244
245     // Currently only bin and lib files get formatted.
246     run_rustfmt(&targets, &rustfmt_args, verbosity)
247 }
248
249 /// Target uses a `path` field for equality and hashing.
250 #[derive(Debug)]
251 pub struct Target {
252     /// A path to the main source file of the target.
253     path: PathBuf,
254     /// A kind of target (e.g., lib, bin, example, ...).
255     kind: String,
256     /// Rust edition for this target.
257     edition: String,
258 }
259
260 impl Target {
261     pub fn from_target(target: &cargo_metadata::Target) -> Self {
262         let path = PathBuf::from(&target.src_path);
263         let canonicalized = fs::canonicalize(&path).unwrap_or(path);
264
265         Target {
266             path: canonicalized,
267             kind: target.kind[0].clone(),
268             edition: target.edition.clone(),
269         }
270     }
271 }
272
273 impl PartialEq for Target {
274     fn eq(&self, other: &Target) -> bool {
275         self.path == other.path
276     }
277 }
278
279 impl PartialOrd for Target {
280     fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
281         Some(self.path.cmp(&other.path))
282     }
283 }
284
285 impl Ord for Target {
286     fn cmp(&self, other: &Target) -> Ordering {
287         self.path.cmp(&other.path)
288     }
289 }
290
291 impl Eq for Target {}
292
293 impl Hash for Target {
294     fn hash<H: Hasher>(&self, state: &mut H) {
295         self.path.hash(state);
296     }
297 }
298
299 #[derive(Debug, PartialEq, Eq)]
300 pub enum CargoFmtStrategy {
301     /// Format every packages and dependencies.
302     All,
303     /// Format packages that are specified by the command line argument.
304     Some(Vec<String>),
305     /// Format the root packages only.
306     Root,
307 }
308
309 impl CargoFmtStrategy {
310     pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
311         match (opts.format_all, opts.packages.is_empty()) {
312             (false, true) => CargoFmtStrategy::Root,
313             (true, _) => CargoFmtStrategy::All,
314             (false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
315         }
316     }
317 }
318
319 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
320 fn get_targets(
321     strategy: &CargoFmtStrategy,
322     manifest_path: Option<&Path>,
323 ) -> Result<BTreeSet<Target>, io::Error> {
324     let mut targets = BTreeSet::new();
325
326     match *strategy {
327         CargoFmtStrategy::Root => get_targets_root_only(manifest_path, &mut targets)?,
328         CargoFmtStrategy::All => {
329             get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())?
330         }
331         CargoFmtStrategy::Some(ref hitlist) => {
332             get_targets_with_hitlist(manifest_path, hitlist, &mut targets)?
333         }
334     }
335
336     if targets.is_empty() {
337         Err(io::Error::new(
338             io::ErrorKind::Other,
339             "Failed to find targets".to_owned(),
340         ))
341     } else {
342         Ok(targets)
343     }
344 }
345
346 fn get_targets_root_only(
347     manifest_path: Option<&Path>,
348     targets: &mut BTreeSet<Target>,
349 ) -> Result<(), io::Error> {
350     let metadata = get_cargo_metadata(manifest_path, false)?;
351     let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
352     let (in_workspace_root, current_dir_manifest) = if let Some(target_manifest) = manifest_path {
353         (
354             workspace_root_path == target_manifest,
355             target_manifest.canonicalize()?,
356         )
357     } else {
358         let current_dir = env::current_dir()?.canonicalize()?;
359         (
360             workspace_root_path == current_dir,
361             current_dir.join("Cargo.toml"),
362         )
363     };
364
365     let package_targets = match metadata.packages.len() {
366         1 => metadata.packages.into_iter().next().unwrap().targets,
367         _ => metadata
368             .packages
369             .into_iter()
370             .filter(|p| {
371                 in_workspace_root
372                     || PathBuf::from(&p.manifest_path)
373                         .canonicalize()
374                         .unwrap_or_default()
375                         == current_dir_manifest
376             })
377             .map(|p| p.targets)
378             .flatten()
379             .collect(),
380     };
381
382     for target in package_targets {
383         targets.insert(Target::from_target(&target));
384     }
385
386     Ok(())
387 }
388
389 fn get_targets_recursive(
390     manifest_path: Option<&Path>,
391     mut targets: &mut BTreeSet<Target>,
392     visited: &mut BTreeSet<String>,
393 ) -> Result<(), io::Error> {
394     let metadata = get_cargo_metadata(manifest_path, false)?;
395     let metadata_with_deps = get_cargo_metadata(manifest_path, true)?;
396
397     for package in metadata.packages {
398         add_targets(&package.targets, &mut targets);
399
400         // Look for local dependencies.
401         for dependency in package.dependencies {
402             if dependency.source.is_some() || visited.contains(&dependency.name) {
403                 continue;
404             }
405
406             let dependency_package = metadata_with_deps
407                 .packages
408                 .iter()
409                 .find(|p| p.name == dependency.name && p.source.is_none());
410             let manifest_path = if dependency_package.is_some() {
411                 PathBuf::from(&dependency_package.unwrap().manifest_path)
412             } else {
413                 let mut package_manifest_path = PathBuf::from(&package.manifest_path);
414                 package_manifest_path.pop();
415                 package_manifest_path.push(&dependency.name);
416                 package_manifest_path.push("Cargo.toml");
417                 package_manifest_path
418             };
419
420             if manifest_path.exists() {
421                 visited.insert(dependency.name);
422                 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
423             }
424         }
425     }
426
427     Ok(())
428 }
429
430 fn get_targets_with_hitlist(
431     manifest_path: Option<&Path>,
432     hitlist: &[String],
433     targets: &mut BTreeSet<Target>,
434 ) -> Result<(), io::Error> {
435     let metadata = get_cargo_metadata(manifest_path, false)?;
436
437     let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
438
439     for package in metadata.packages {
440         if workspace_hitlist.remove(&package.name) {
441             for target in package.targets {
442                 targets.insert(Target::from_target(&target));
443             }
444         }
445     }
446
447     if workspace_hitlist.is_empty() {
448         Ok(())
449     } else {
450         let package = workspace_hitlist.iter().next().unwrap();
451         Err(io::Error::new(
452             io::ErrorKind::InvalidInput,
453             format!("package `{}` is not a member of the workspace", package),
454         ))
455     }
456 }
457
458 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
459     for target in target_paths {
460         targets.insert(Target::from_target(target));
461     }
462 }
463
464 fn run_rustfmt(
465     targets: &BTreeSet<Target>,
466     fmt_args: &[String],
467     verbosity: Verbosity,
468 ) -> Result<i32, io::Error> {
469     let by_edition = targets
470         .iter()
471         .inspect(|t| {
472             if verbosity == Verbosity::Verbose {
473                 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
474             }
475         })
476         .fold(BTreeMap::new(), |mut h, t| {
477             h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
478             h
479         });
480
481     let mut status = vec![];
482     for (edition, files) in by_edition {
483         let stdout = if verbosity == Verbosity::Quiet {
484             std::process::Stdio::null()
485         } else {
486             std::process::Stdio::inherit()
487         };
488
489         if verbosity == Verbosity::Verbose {
490             print!("rustfmt");
491             print!(" --edition {}", edition);
492             fmt_args.iter().for_each(|f| print!(" {}", f));
493             files.iter().for_each(|f| print!(" {}", f.display()));
494             println!();
495         }
496
497         let mut command = rustfmt_command()
498             .stdout(stdout)
499             .args(files)
500             .args(&["--edition", edition])
501             .args(fmt_args)
502             .spawn()
503             .map_err(|e| match e.kind() {
504                 io::ErrorKind::NotFound => io::Error::new(
505                     io::ErrorKind::Other,
506                     "Could not run rustfmt, please make sure it is in your PATH.",
507                 ),
508                 _ => e,
509             })?;
510
511         status.push(command.wait()?);
512     }
513
514     Ok(status
515         .iter()
516         .filter_map(|s| if s.success() { None } else { s.code() })
517         .next()
518         .unwrap_or(SUCCESS))
519 }
520
521 fn get_cargo_metadata(
522     manifest_path: Option<&Path>,
523     include_deps: bool,
524 ) -> Result<cargo_metadata::Metadata, io::Error> {
525     let mut cmd = cargo_metadata::MetadataCommand::new();
526     if !include_deps {
527         cmd.no_deps();
528     }
529     if let Some(manifest_path) = manifest_path {
530         cmd.manifest_path(manifest_path);
531     }
532     cmd.other_options(&[String::from("--offline")]);
533
534     match cmd.exec() {
535         Ok(metadata) => Ok(metadata),
536         Err(_) => {
537             cmd.other_options(vec![]);
538             match cmd.exec() {
539                 Ok(metadata) => Ok(metadata),
540                 Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
541             }
542         }
543     }
544 }
545
546 #[cfg(test)]
547 mod cargo_fmt_tests {
548     use super::*;
549
550     #[test]
551     fn default_options() {
552         let empty: Vec<String> = vec![];
553         let o = Opts::from_iter(&empty);
554         assert_eq!(false, o.quiet);
555         assert_eq!(false, o.verbose);
556         assert_eq!(false, o.version);
557         assert_eq!(empty, o.packages);
558         assert_eq!(empty, o.rustfmt_options);
559         assert_eq!(false, o.format_all);
560         assert_eq!(None, o.manifest_path);
561         assert_eq!(None, o.message_format);
562     }
563
564     #[test]
565     fn good_options() {
566         let o = Opts::from_iter(&[
567             "test",
568             "-q",
569             "-p",
570             "p1",
571             "-p",
572             "p2",
573             "--message-format",
574             "short",
575             "--",
576             "--edition",
577             "2018",
578         ]);
579         assert_eq!(true, o.quiet);
580         assert_eq!(false, o.verbose);
581         assert_eq!(false, o.version);
582         assert_eq!(vec!["p1", "p2"], o.packages);
583         assert_eq!(vec!["--edition", "2018"], o.rustfmt_options);
584         assert_eq!(false, o.format_all);
585         assert_eq!(Some(String::from("short")), o.message_format);
586     }
587
588     #[test]
589     fn unexpected_option() {
590         assert!(
591             Opts::clap()
592                 .get_matches_from_safe(&["test", "unexpected"])
593                 .is_err()
594         );
595     }
596
597     #[test]
598     fn unexpected_flag() {
599         assert!(
600             Opts::clap()
601                 .get_matches_from_safe(&["test", "--flag"])
602                 .is_err()
603         );
604     }
605
606     #[test]
607     fn mandatory_separator() {
608         assert!(
609             Opts::clap()
610                 .get_matches_from_safe(&["test", "--check"])
611                 .is_err()
612         );
613         assert!(
614             !Opts::clap()
615                 .get_matches_from_safe(&["test", "--", "--check"])
616                 .is_err()
617         );
618     }
619
620     #[test]
621     fn multiple_packages_one_by_one() {
622         let o = Opts::from_iter(&[
623             "test",
624             "-p",
625             "package1",
626             "--package",
627             "package2",
628             "-p",
629             "package3",
630         ]);
631         assert_eq!(3, o.packages.len());
632     }
633
634     #[test]
635     fn multiple_packages_grouped() {
636         let o = Opts::from_iter(&[
637             "test",
638             "--package",
639             "package1",
640             "package2",
641             "-p",
642             "package3",
643             "package4",
644         ]);
645         assert_eq!(4, o.packages.len());
646     }
647
648     #[test]
649     fn empty_packages_1() {
650         assert!(Opts::clap().get_matches_from_safe(&["test", "-p"]).is_err());
651     }
652
653     #[test]
654     fn empty_packages_2() {
655         assert!(
656             Opts::clap()
657                 .get_matches_from_safe(&["test", "-p", "--", "--check"])
658                 .is_err()
659         );
660     }
661
662     #[test]
663     fn empty_packages_3() {
664         assert!(
665             Opts::clap()
666                 .get_matches_from_safe(&["test", "-p", "--verbose"])
667                 .is_err()
668         );
669     }
670
671     #[test]
672     fn empty_packages_4() {
673         assert!(
674             Opts::clap()
675                 .get_matches_from_safe(&["test", "-p", "--check"])
676                 .is_err()
677         );
678     }
679
680     mod convert_message_format_to_rustfmt_args_tests {
681         use super::*;
682
683         #[test]
684         fn invalid_message_format() {
685             assert_eq!(
686                 convert_message_format_to_rustfmt_args("awesome", &mut vec![]),
687                 Err(String::from(
688                     "invalid --message-format value: awesome. Allowed values are: short|json|human"
689                 )),
690             );
691         }
692
693         #[test]
694         fn json_message_format_and_check_arg() {
695             let mut args = vec![String::from("--check")];
696             assert_eq!(
697                 convert_message_format_to_rustfmt_args("json", &mut args),
698                 Err(String::from(
699                     "cannot include --check arg when --message-format is set to json"
700                 )),
701             );
702         }
703
704         #[test]
705         fn json_message_format_and_emit_arg() {
706             let mut args = vec![String::from("--emit"), String::from("checkstyle")];
707             assert_eq!(
708                 convert_message_format_to_rustfmt_args("json", &mut args),
709                 Err(String::from(
710                     "cannot include --emit arg when --message-format is set to json"
711                 )),
712             );
713         }
714
715         #[test]
716         fn json_message_format() {
717             let mut args = vec![String::from("--edition"), String::from("2018")];
718             assert!(convert_message_format_to_rustfmt_args("json", &mut args).is_ok());
719             assert_eq!(
720                 args,
721                 vec![
722                     String::from("--edition"),
723                     String::from("2018"),
724                     String::from("--emit"),
725                     String::from("json")
726                 ]
727             );
728         }
729
730         #[test]
731         fn human_message_format() {
732             let exp_args = vec![String::from("--emit"), String::from("json")];
733             let mut act_args = exp_args.clone();
734             assert!(convert_message_format_to_rustfmt_args("human", &mut act_args).is_ok());
735             assert_eq!(act_args, exp_args);
736         }
737
738         #[test]
739         fn short_message_format() {
740             let mut args = vec![String::from("--check")];
741             assert!(convert_message_format_to_rustfmt_args("short", &mut args).is_ok());
742             assert_eq!(args, vec![String::from("--check"), String::from("-l")]);
743         }
744
745         #[test]
746         fn short_message_format_included_short_list_files_flag() {
747             let mut args = vec![String::from("--check"), String::from("-l")];
748             assert!(convert_message_format_to_rustfmt_args("short", &mut args).is_ok());
749             assert_eq!(args, vec![String::from("--check"), String::from("-l")]);
750         }
751
752         #[test]
753         fn short_message_format_included_long_list_files_flag() {
754             let mut args = vec![String::from("--check"), String::from("--files-with-diff")];
755             assert!(convert_message_format_to_rustfmt_args("short", &mut args).is_ok());
756             assert_eq!(
757                 args,
758                 vec![String::from("--check"), String::from("--files-with-diff")]
759             );
760         }
761     }
762 }