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