]> git.lizzy.rs Git - rust.git/blob - src/cargo-fmt/main.rs
Merge pull request #3577 from topecongiro/issue-3575
[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::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     author = "",
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     /// Options passed to rustfmt
45     // 'raw = true' to make `--` explicit.
46     #[structopt(name = "rustfmt_options", raw(raw = "true"))]
47     rustfmt_options: Vec<String>,
48
49     /// Format all packages (only usable in workspaces)
50     #[structopt(long = "all")]
51     format_all: bool,
52 }
53
54 fn main() {
55     let exit_status = execute();
56     std::io::stdout().flush().unwrap();
57     std::process::exit(exit_status);
58 }
59
60 const SUCCESS: i32 = 0;
61 const FAILURE: i32 = 1;
62
63 fn execute() -> i32 {
64     // Drop extra `fmt` argument provided by `cargo`.
65     let mut found_fmt = false;
66     let args = env::args().filter(|x| {
67         if found_fmt {
68             true
69         } else {
70             found_fmt = x == "fmt";
71             x != "fmt"
72         }
73     });
74
75     let opts = Opts::from_iter(args);
76
77     let verbosity = match (opts.verbose, opts.quiet) {
78         (false, false) => Verbosity::Normal,
79         (false, true) => Verbosity::Quiet,
80         (true, false) => Verbosity::Verbose,
81         (true, true) => {
82             print_usage_to_stderr("quiet mode and verbose mode are not compatible");
83             return FAILURE;
84         }
85     };
86
87     if opts.version {
88         return handle_command_status(get_version());
89     }
90
91     let strategy = CargoFmtStrategy::from_opts(&opts);
92
93     handle_command_status(format_crate(verbosity, &strategy, opts.rustfmt_options))
94 }
95
96 fn print_usage_to_stderr(reason: &str) {
97     eprintln!("{}", reason);
98     let app = Opts::clap();
99     app.write_help(&mut io::stderr())
100         .expect("failed to write to stderr");
101 }
102
103 #[derive(Debug, Clone, Copy, PartialEq)]
104 pub enum Verbosity {
105     Verbose,
106     Normal,
107     Quiet,
108 }
109
110 fn handle_command_status(status: Result<i32, io::Error>) -> i32 {
111     match status {
112         Err(e) => {
113             print_usage_to_stderr(&e.to_string());
114             FAILURE
115         }
116         Ok(status) => status,
117     }
118 }
119
120 fn get_version() -> Result<i32, io::Error> {
121     let mut command = Command::new("rustfmt")
122         .stdout(std::process::Stdio::inherit())
123         .args(&[String::from("--version")])
124         .spawn()
125         .map_err(|e| match e.kind() {
126             io::ErrorKind::NotFound => io::Error::new(
127                 io::ErrorKind::Other,
128                 "Could not run rustfmt, please make sure it is in your PATH.",
129             ),
130             _ => e,
131         })?;
132     let result = command.wait()?;
133     if result.success() {
134         Ok(SUCCESS)
135     } else {
136         Ok(result.code().unwrap_or(SUCCESS))
137     }
138 }
139
140 fn format_crate(
141     verbosity: Verbosity,
142     strategy: &CargoFmtStrategy,
143     rustfmt_args: Vec<String>,
144 ) -> Result<i32, io::Error> {
145     let targets = if rustfmt_args
146         .iter()
147         .any(|s| ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str()))
148     {
149         BTreeSet::new()
150     } else {
151         get_targets(strategy)?
152     };
153
154     // Currently only bin and lib files get formatted.
155     run_rustfmt(&targets, &rustfmt_args, verbosity)
156 }
157
158 /// Target uses a `path` field for equality and hashing.
159 #[derive(Debug)]
160 pub struct Target {
161     /// A path to the main source file of the target.
162     path: PathBuf,
163     /// A kind of target (e.g., lib, bin, example, ...).
164     kind: String,
165     /// Rust edition for this target.
166     edition: String,
167 }
168
169 impl Target {
170     pub fn from_target(target: &cargo_metadata::Target) -> Self {
171         let path = PathBuf::from(&target.src_path);
172         let canonicalized = fs::canonicalize(&path).unwrap_or(path);
173
174         Target {
175             path: canonicalized,
176             kind: target.kind[0].clone(),
177             edition: target.edition.clone(),
178         }
179     }
180 }
181
182 impl PartialEq for Target {
183     fn eq(&self, other: &Target) -> bool {
184         self.path == other.path
185     }
186 }
187
188 impl PartialOrd for Target {
189     fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
190         Some(self.path.cmp(&other.path))
191     }
192 }
193
194 impl Ord for Target {
195     fn cmp(&self, other: &Target) -> Ordering {
196         self.path.cmp(&other.path)
197     }
198 }
199
200 impl Eq for Target {}
201
202 impl Hash for Target {
203     fn hash<H: Hasher>(&self, state: &mut H) {
204         self.path.hash(state);
205     }
206 }
207
208 #[derive(Debug, PartialEq, Eq)]
209 pub enum CargoFmtStrategy {
210     /// Format every packages and dependencies.
211     All,
212     /// Format packages that are specified by the command line argument.
213     Some(Vec<String>),
214     /// Format the root packages only.
215     Root,
216 }
217
218 impl CargoFmtStrategy {
219     pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
220         match (opts.format_all, opts.packages.is_empty()) {
221             (false, true) => CargoFmtStrategy::Root,
222             (true, _) => CargoFmtStrategy::All,
223             (false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
224         }
225     }
226 }
227
228 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
229 fn get_targets(strategy: &CargoFmtStrategy) -> Result<BTreeSet<Target>, io::Error> {
230     let mut targets = BTreeSet::new();
231
232     match *strategy {
233         CargoFmtStrategy::Root => get_targets_root_only(&mut targets)?,
234         CargoFmtStrategy::All => get_targets_recursive(None, &mut targets, &mut BTreeSet::new())?,
235         CargoFmtStrategy::Some(ref hitlist) => get_targets_with_hitlist(hitlist, &mut targets)?,
236     }
237
238     if targets.is_empty() {
239         Err(io::Error::new(
240             io::ErrorKind::Other,
241             "Failed to find targets".to_owned(),
242         ))
243     } else {
244         Ok(targets)
245     }
246 }
247
248 fn get_targets_root_only(targets: &mut BTreeSet<Target>) -> Result<(), io::Error> {
249     let metadata = get_cargo_metadata(None)?;
250     let current_dir = env::current_dir()?.canonicalize()?;
251     let current_dir_manifest = current_dir.join("Cargo.toml");
252     let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
253     let in_workspace_root = workspace_root_path == current_dir;
254
255     for package in metadata.packages {
256         if in_workspace_root || PathBuf::from(&package.manifest_path) == current_dir_manifest {
257             for target in package.targets {
258                 targets.insert(Target::from_target(&target));
259             }
260         }
261     }
262
263     Ok(())
264 }
265
266 fn get_targets_recursive(
267     manifest_path: Option<&Path>,
268     mut targets: &mut BTreeSet<Target>,
269     visited: &mut BTreeSet<String>,
270 ) -> Result<(), io::Error> {
271     let metadata = get_cargo_metadata(manifest_path)?;
272
273     for package in metadata.packages {
274         add_targets(&package.targets, &mut targets);
275
276         // Look for local dependencies.
277         for dependency in package.dependencies {
278             if dependency.source.is_some() || visited.contains(&dependency.name) {
279                 continue;
280             }
281
282             let mut manifest_path = PathBuf::from(&package.manifest_path);
283
284             manifest_path.pop();
285             manifest_path.push(&dependency.name);
286             manifest_path.push("Cargo.toml");
287
288             if manifest_path.exists() {
289                 visited.insert(dependency.name);
290                 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
291             }
292         }
293     }
294
295     Ok(())
296 }
297
298 fn get_targets_with_hitlist(
299     hitlist: &[String],
300     targets: &mut BTreeSet<Target>,
301 ) -> Result<(), io::Error> {
302     let metadata = get_cargo_metadata(None)?;
303
304     let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
305
306     for package in metadata.packages {
307         if workspace_hitlist.remove(&package.name) {
308             for target in package.targets {
309                 targets.insert(Target::from_target(&target));
310             }
311         }
312     }
313
314     if workspace_hitlist.is_empty() {
315         Ok(())
316     } else {
317         let package = workspace_hitlist.iter().next().unwrap();
318         Err(io::Error::new(
319             io::ErrorKind::InvalidInput,
320             format!("package `{}` is not a member of the workspace", package),
321         ))
322     }
323 }
324
325 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
326     for target in target_paths {
327         targets.insert(Target::from_target(target));
328     }
329 }
330
331 fn run_rustfmt(
332     targets: &BTreeSet<Target>,
333     fmt_args: &[String],
334     verbosity: Verbosity,
335 ) -> Result<i32, io::Error> {
336     let by_edition = targets
337         .iter()
338         .inspect(|t| {
339             if verbosity == Verbosity::Verbose {
340                 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
341             }
342         })
343         .fold(BTreeMap::new(), |mut h, t| {
344             h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
345             h
346         });
347
348     let mut status = vec![];
349     for (edition, files) in by_edition {
350         let stdout = if verbosity == Verbosity::Quiet {
351             std::process::Stdio::null()
352         } else {
353             std::process::Stdio::inherit()
354         };
355
356         if verbosity == Verbosity::Verbose {
357             print!("rustfmt");
358             print!(" --edition {}", edition);
359             fmt_args.iter().for_each(|f| print!(" {}", f));
360             files.iter().for_each(|f| print!(" {}", f.display()));
361             println!();
362         }
363
364         let mut command = Command::new("rustfmt")
365             .stdout(stdout)
366             .args(files)
367             .args(&["--edition", edition])
368             .args(fmt_args)
369             .spawn()
370             .map_err(|e| match e.kind() {
371                 io::ErrorKind::NotFound => io::Error::new(
372                     io::ErrorKind::Other,
373                     "Could not run rustfmt, please make sure it is in your PATH.",
374                 ),
375                 _ => e,
376             })?;
377
378         status.push(command.wait()?);
379     }
380
381     Ok(status
382         .iter()
383         .filter_map(|s| if s.success() { None } else { s.code() })
384         .next()
385         .unwrap_or(SUCCESS))
386 }
387
388 fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
389     let mut cmd = cargo_metadata::MetadataCommand::new();
390     cmd.no_deps();
391     if let Some(manifest_path) = manifest_path {
392         cmd.manifest_path(manifest_path);
393     }
394     match cmd.exec() {
395         Ok(metadata) => Ok(metadata),
396         Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
397     }
398 }
399
400 #[cfg(test)]
401 mod cargo_fmt_tests {
402     use super::*;
403
404     #[test]
405     fn default_options() {
406         let empty: Vec<String> = vec![];
407         let o = Opts::from_iter(&empty);
408         assert_eq!(false, o.quiet);
409         assert_eq!(false, o.verbose);
410         assert_eq!(false, o.version);
411         assert_eq!(empty, o.packages);
412         assert_eq!(empty, o.rustfmt_options);
413         assert_eq!(false, o.format_all);
414     }
415
416     #[test]
417     fn good_options() {
418         let o = Opts::from_iter(&[
419             "test",
420             "-q",
421             "-p",
422             "p1",
423             "-p",
424             "p2",
425             "--",
426             "--edition",
427             "2018",
428         ]);
429         assert_eq!(true, o.quiet);
430         assert_eq!(false, o.verbose);
431         assert_eq!(false, o.version);
432         assert_eq!(vec!["p1", "p2"], o.packages);
433         assert_eq!(vec!["--edition", "2018"], o.rustfmt_options);
434         assert_eq!(false, o.format_all);
435     }
436
437     #[test]
438     fn unexpected_option() {
439         assert!(
440             Opts::clap()
441                 .get_matches_from_safe(&["test", "unexpected"])
442                 .is_err()
443         );
444     }
445
446     #[test]
447     fn unexpected_flag() {
448         assert!(
449             Opts::clap()
450                 .get_matches_from_safe(&["test", "--flag"])
451                 .is_err()
452         );
453     }
454
455     #[test]
456     fn mandatory_separator() {
457         assert!(
458             Opts::clap()
459                 .get_matches_from_safe(&["test", "--check"])
460                 .is_err()
461         );
462         assert!(
463             !Opts::clap()
464                 .get_matches_from_safe(&["test", "--", "--check"])
465                 .is_err()
466         );
467     }
468
469     #[test]
470     fn multiple_packages_one_by_one() {
471         let o = Opts::from_iter(&[
472             "test",
473             "-p",
474             "package1",
475             "--package",
476             "package2",
477             "-p",
478             "package3",
479         ]);
480         assert_eq!(3, o.packages.len());
481     }
482
483     #[test]
484     fn multiple_packages_grouped() {
485         let o = Opts::from_iter(&[
486             "test",
487             "--package",
488             "package1",
489             "package2",
490             "-p",
491             "package3",
492             "package4",
493         ]);
494         assert_eq!(4, o.packages.len());
495     }
496
497     #[test]
498     fn empty_packages_1() {
499         assert!(Opts::clap().get_matches_from_safe(&["test", "-p"]).is_err());
500     }
501
502     #[test]
503     fn empty_packages_2() {
504         assert!(
505             Opts::clap()
506                 .get_matches_from_safe(&["test", "-p", "--", "--check"])
507                 .is_err()
508         );
509     }
510
511     #[test]
512     fn empty_packages_3() {
513         assert!(
514             Opts::clap()
515                 .get_matches_from_safe(&["test", "-p", "--verbose"])
516                 .is_err()
517         );
518     }
519
520     #[test]
521     fn empty_packages_4() {
522         assert!(
523             Opts::clap()
524                 .get_matches_from_safe(&["test", "-p", "--check"])
525                 .is_err()
526         );
527     }
528 }