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