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 #[path = "test/mod.rs"]
21 #[cfg(test)]
22 mod cargo_fmt_tests;
23 
24 #[derive(StructOpt, Debug)]
25 #[structopt(
26     bin_name = "cargo fmt",
27     about = "This utility formats all bin and lib files of \
28              the current crate using rustfmt."
29 )]
30 pub struct Opts {
31     /// No output printed to stdout
32     #[structopt(short = "q", long = "quiet")]
33     quiet: bool,
34 
35     /// Use verbose output
36     #[structopt(short = "v", long = "verbose")]
37     verbose: bool,
38 
39     /// Print rustfmt version and exit
40     #[structopt(long = "version")]
41     version: bool,
42 
43     /// Specify package to format
44     #[structopt(short = "p", long = "package", value_name = "package")]
45     packages: Vec<String>,
46 
47     /// Specify path to Cargo.toml
48     #[structopt(long = "manifest-path", value_name = "manifest-path")]
49     manifest_path: Option<String>,
50 
51     /// Specify message-format: short|json|human
52     #[structopt(long = "message-format", value_name = "message-format")]
53     message_format: Option<String>,
54 
55     /// Options passed to rustfmt
56     // 'raw = true' to make `--` explicit.
57     #[structopt(name = "rustfmt_options", raw(true))]
58     rustfmt_options: Vec<String>,
59 
60     /// Format all packages, and also their local path-based dependencies
61     #[structopt(long = "all")]
62     format_all: bool,
63 
64     /// Run rustfmt in check mode
65     #[structopt(long = "check")]
66     check: bool,
67 }
68 
main()69 fn main() {
70     let exit_status = execute();
71     std::io::stdout().flush().unwrap();
72     std::process::exit(exit_status);
73 }
74 
75 const SUCCESS: i32 = 0;
76 const FAILURE: i32 = 1;
77 
execute() -> i3278 fn execute() -> i32 {
79     // Drop extra `fmt` argument provided by `cargo`.
80     let mut found_fmt = false;
81     let args = env::args().filter(|x| {
82         if found_fmt {
83             true
84         } else {
85             found_fmt = x == "fmt";
86             x != "fmt"
87         }
88     });
89 
90     let opts = Opts::from_iter(args);
91 
92     let verbosity = match (opts.verbose, opts.quiet) {
93         (false, false) => Verbosity::Normal,
94         (false, true) => Verbosity::Quiet,
95         (true, false) => Verbosity::Verbose,
96         (true, true) => {
97             print_usage_to_stderr("quiet mode and verbose mode are not compatible");
98             return FAILURE;
99         }
100     };
101 
102     if opts.version {
103         return handle_command_status(get_rustfmt_info(&[String::from("--version")]));
104     }
105     if opts.rustfmt_options.iter().any(|s| {
106         ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str())
107             || s.starts_with("--help=")
108             || s.starts_with("--print-config=")
109     }) {
110         return handle_command_status(get_rustfmt_info(&opts.rustfmt_options));
111     }
112 
113     let strategy = CargoFmtStrategy::from_opts(&opts);
114     let mut rustfmt_args = opts.rustfmt_options;
115     if opts.check {
116         let check_flag = "--check";
117         if !rustfmt_args.iter().any(|o| o == check_flag) {
118             rustfmt_args.push(check_flag.to_owned());
119         }
120     }
121     if let Some(message_format) = opts.message_format {
122         if let Err(msg) = convert_message_format_to_rustfmt_args(&message_format, &mut rustfmt_args)
123         {
124             print_usage_to_stderr(&msg);
125             return FAILURE;
126         }
127     }
128 
129     if let Some(specified_manifest_path) = opts.manifest_path {
130         if !specified_manifest_path.ends_with("Cargo.toml") {
131             print_usage_to_stderr("the manifest-path must be a path to a Cargo.toml file");
132             return FAILURE;
133         }
134         let manifest_path = PathBuf::from(specified_manifest_path);
135         handle_command_status(format_crate(
136             verbosity,
137             &strategy,
138             rustfmt_args,
139             Some(&manifest_path),
140         ))
141     } else {
142         handle_command_status(format_crate(verbosity, &strategy, rustfmt_args, None))
143     }
144 }
145 
rustfmt_command() -> Command146 fn rustfmt_command() -> Command {
147     let rustfmt_var = env::var_os("RUSTFMT");
148     let rustfmt = match &rustfmt_var {
149         Some(rustfmt) => rustfmt,
150         None => OsStr::new("rustfmt"),
151     };
152     Command::new(rustfmt)
153 }
154 
convert_message_format_to_rustfmt_args( message_format: &str, rustfmt_args: &mut Vec<String>, ) -> Result<(), String>155 fn convert_message_format_to_rustfmt_args(
156     message_format: &str,
157     rustfmt_args: &mut Vec<String>,
158 ) -> Result<(), String> {
159     let mut contains_emit_mode = false;
160     let mut contains_check = false;
161     let mut contains_list_files = false;
162     for arg in rustfmt_args.iter() {
163         if arg.starts_with("--emit") {
164             contains_emit_mode = true;
165         }
166         if arg == "--check" {
167             contains_check = true;
168         }
169         if arg == "-l" || arg == "--files-with-diff" {
170             contains_list_files = true;
171         }
172     }
173     match message_format {
174         "short" => {
175             if !contains_list_files {
176                 rustfmt_args.push(String::from("-l"));
177             }
178             Ok(())
179         }
180         "json" => {
181             if contains_emit_mode {
182                 return Err(String::from(
183                     "cannot include --emit arg when --message-format is set to json",
184                 ));
185             }
186             if contains_check {
187                 return Err(String::from(
188                     "cannot include --check arg when --message-format is set to json",
189                 ));
190             }
191             rustfmt_args.push(String::from("--emit"));
192             rustfmt_args.push(String::from("json"));
193             Ok(())
194         }
195         "human" => Ok(()),
196         _ => {
197             return Err(format!(
198                 "invalid --message-format value: {}. Allowed values are: short|json|human",
199                 message_format
200             ));
201         }
202     }
203 }
204 
print_usage_to_stderr(reason: &str)205 fn print_usage_to_stderr(reason: &str) {
206     eprintln!("{}", reason);
207     let app = Opts::clap();
208     app.after_help("")
209         .write_help(&mut io::stderr())
210         .expect("failed to write to stderr");
211 }
212 
213 #[derive(Debug, Clone, Copy, PartialEq)]
214 pub enum Verbosity {
215     Verbose,
216     Normal,
217     Quiet,
218 }
219 
handle_command_status(status: Result<i32, io::Error>) -> i32220 fn handle_command_status(status: Result<i32, io::Error>) -> i32 {
221     match status {
222         Err(e) => {
223             print_usage_to_stderr(&e.to_string());
224             FAILURE
225         }
226         Ok(status) => status,
227     }
228 }
229 
get_rustfmt_info(args: &[String]) -> Result<i32, io::Error>230 fn get_rustfmt_info(args: &[String]) -> Result<i32, io::Error> {
231     let mut command = rustfmt_command()
232         .stdout(std::process::Stdio::inherit())
233         .args(args)
234         .spawn()
235         .map_err(|e| match e.kind() {
236             io::ErrorKind::NotFound => io::Error::new(
237                 io::ErrorKind::Other,
238                 "Could not run rustfmt, please make sure it is in your PATH.",
239             ),
240             _ => e,
241         })?;
242     let result = command.wait()?;
243     if result.success() {
244         Ok(SUCCESS)
245     } else {
246         Ok(result.code().unwrap_or(SUCCESS))
247     }
248 }
249 
format_crate( verbosity: Verbosity, strategy: &CargoFmtStrategy, rustfmt_args: Vec<String>, manifest_path: Option<&Path>, ) -> Result<i32, io::Error>250 fn format_crate(
251     verbosity: Verbosity,
252     strategy: &CargoFmtStrategy,
253     rustfmt_args: Vec<String>,
254     manifest_path: Option<&Path>,
255 ) -> Result<i32, io::Error> {
256     let targets = get_targets(strategy, manifest_path)?;
257 
258     // Currently only bin and lib files get formatted.
259     run_rustfmt(&targets, &rustfmt_args, verbosity)
260 }
261 
262 /// Target uses a `path` field for equality and hashing.
263 #[derive(Debug)]
264 pub struct Target {
265     /// A path to the main source file of the target.
266     path: PathBuf,
267     /// A kind of target (e.g., lib, bin, example, ...).
268     kind: String,
269     /// Rust edition for this target.
270     edition: String,
271 }
272 
273 impl Target {
from_target(target: &cargo_metadata::Target) -> Self274     pub fn from_target(target: &cargo_metadata::Target) -> Self {
275         let path = PathBuf::from(&target.src_path);
276         let canonicalized = fs::canonicalize(&path).unwrap_or(path);
277 
278         Target {
279             path: canonicalized,
280             kind: target.kind[0].clone(),
281             edition: target.edition.clone(),
282         }
283     }
284 }
285 
286 impl PartialEq for Target {
eq(&self, other: &Target) -> bool287     fn eq(&self, other: &Target) -> bool {
288         self.path == other.path
289     }
290 }
291 
292 impl PartialOrd for Target {
partial_cmp(&self, other: &Target) -> Option<Ordering>293     fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
294         Some(self.path.cmp(&other.path))
295     }
296 }
297 
298 impl Ord for Target {
cmp(&self, other: &Target) -> Ordering299     fn cmp(&self, other: &Target) -> Ordering {
300         self.path.cmp(&other.path)
301     }
302 }
303 
304 impl Eq for Target {}
305 
306 impl Hash for Target {
hash<H: Hasher>(&self, state: &mut H)307     fn hash<H: Hasher>(&self, state: &mut H) {
308         self.path.hash(state);
309     }
310 }
311 
312 #[derive(Debug, PartialEq, Eq)]
313 pub enum CargoFmtStrategy {
314     /// Format every packages and dependencies.
315     All,
316     /// Format packages that are specified by the command line argument.
317     Some(Vec<String>),
318     /// Format the root packages only.
319     Root,
320 }
321 
322 impl CargoFmtStrategy {
from_opts(opts: &Opts) -> CargoFmtStrategy323     pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
324         match (opts.format_all, opts.packages.is_empty()) {
325             (false, true) => CargoFmtStrategy::Root,
326             (true, _) => CargoFmtStrategy::All,
327             (false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
328         }
329     }
330 }
331 
332 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
get_targets( strategy: &CargoFmtStrategy, manifest_path: Option<&Path>, ) -> Result<BTreeSet<Target>, io::Error>333 fn get_targets(
334     strategy: &CargoFmtStrategy,
335     manifest_path: Option<&Path>,
336 ) -> Result<BTreeSet<Target>, io::Error> {
337     let mut targets = BTreeSet::new();
338 
339     match *strategy {
340         CargoFmtStrategy::Root => get_targets_root_only(manifest_path, &mut targets)?,
341         CargoFmtStrategy::All => {
342             get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())?
343         }
344         CargoFmtStrategy::Some(ref hitlist) => {
345             get_targets_with_hitlist(manifest_path, hitlist, &mut targets)?
346         }
347     }
348 
349     if targets.is_empty() {
350         Err(io::Error::new(
351             io::ErrorKind::Other,
352             "Failed to find targets".to_owned(),
353         ))
354     } else {
355         Ok(targets)
356     }
357 }
358 
get_targets_root_only( manifest_path: Option<&Path>, targets: &mut BTreeSet<Target>, ) -> Result<(), io::Error>359 fn get_targets_root_only(
360     manifest_path: Option<&Path>,
361     targets: &mut BTreeSet<Target>,
362 ) -> Result<(), io::Error> {
363     let metadata = get_cargo_metadata(manifest_path)?;
364     let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
365     let (in_workspace_root, current_dir_manifest) = if let Some(target_manifest) = manifest_path {
366         (
367             workspace_root_path == target_manifest,
368             target_manifest.canonicalize()?,
369         )
370     } else {
371         let current_dir = env::current_dir()?.canonicalize()?;
372         (
373             workspace_root_path == current_dir,
374             current_dir.join("Cargo.toml"),
375         )
376     };
377 
378     let package_targets = match metadata.packages.len() {
379         1 => metadata.packages.into_iter().next().unwrap().targets,
380         _ => metadata
381             .packages
382             .into_iter()
383             .filter(|p| {
384                 in_workspace_root
385                     || PathBuf::from(&p.manifest_path)
386                         .canonicalize()
387                         .unwrap_or_default()
388                         == current_dir_manifest
389             })
390             .map(|p| p.targets)
391             .flatten()
392             .collect(),
393     };
394 
395     for target in package_targets {
396         targets.insert(Target::from_target(&target));
397     }
398 
399     Ok(())
400 }
401 
get_targets_recursive( manifest_path: Option<&Path>, targets: &mut BTreeSet<Target>, visited: &mut BTreeSet<String>, ) -> Result<(), io::Error>402 fn get_targets_recursive(
403     manifest_path: Option<&Path>,
404     targets: &mut BTreeSet<Target>,
405     visited: &mut BTreeSet<String>,
406 ) -> Result<(), io::Error> {
407     let metadata = get_cargo_metadata(manifest_path)?;
408     for package in &metadata.packages {
409         add_targets(&package.targets, targets);
410 
411         // Look for local dependencies using information available since cargo v1.51
412         // It's theoretically possible someone could use a newer version of rustfmt with
413         // a much older version of `cargo`, but we don't try to explicitly support that scenario.
414         // If someone reports an issue with path-based deps not being formatted, be sure to
415         // confirm their version of `cargo` (not `cargo-fmt`) is >= v1.51
416         // https://github.com/rust-lang/cargo/pull/8994
417         for dependency in &package.dependencies {
418             if dependency.path.is_none() || visited.contains(&dependency.name) {
419                 continue;
420             }
421 
422             let manifest_path = PathBuf::from(dependency.path.as_ref().unwrap()).join("Cargo.toml");
423             if manifest_path.exists()
424                 && !metadata
425                     .packages
426                     .iter()
427                     .any(|p| p.manifest_path.eq(&manifest_path))
428             {
429                 visited.insert(dependency.name.to_owned());
430                 get_targets_recursive(Some(&manifest_path), targets, visited)?;
431             }
432         }
433     }
434 
435     Ok(())
436 }
437 
get_targets_with_hitlist( manifest_path: Option<&Path>, hitlist: &[String], targets: &mut BTreeSet<Target>, ) -> Result<(), io::Error>438 fn get_targets_with_hitlist(
439     manifest_path: Option<&Path>,
440     hitlist: &[String],
441     targets: &mut BTreeSet<Target>,
442 ) -> Result<(), io::Error> {
443     let metadata = get_cargo_metadata(manifest_path)?;
444     let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
445 
446     for package in metadata.packages {
447         if workspace_hitlist.remove(&package.name) {
448             for target in package.targets {
449                 targets.insert(Target::from_target(&target));
450             }
451         }
452     }
453 
454     if workspace_hitlist.is_empty() {
455         Ok(())
456     } else {
457         let package = workspace_hitlist.iter().next().unwrap();
458         Err(io::Error::new(
459             io::ErrorKind::InvalidInput,
460             format!("package `{}` is not a member of the workspace", package),
461         ))
462     }
463 }
464 
add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>)465 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
466     for target in target_paths {
467         targets.insert(Target::from_target(target));
468     }
469 }
470 
run_rustfmt( targets: &BTreeSet<Target>, fmt_args: &[String], verbosity: Verbosity, ) -> Result<i32, io::Error>471 fn run_rustfmt(
472     targets: &BTreeSet<Target>,
473     fmt_args: &[String],
474     verbosity: Verbosity,
475 ) -> Result<i32, io::Error> {
476     let by_edition = targets
477         .iter()
478         .inspect(|t| {
479             if verbosity == Verbosity::Verbose {
480                 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
481             }
482         })
483         .fold(BTreeMap::new(), |mut h, t| {
484             h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
485             h
486         });
487 
488     let mut status = vec![];
489     for (edition, files) in by_edition {
490         let stdout = if verbosity == Verbosity::Quiet {
491             std::process::Stdio::null()
492         } else {
493             std::process::Stdio::inherit()
494         };
495 
496         if verbosity == Verbosity::Verbose {
497             print!("rustfmt");
498             print!(" --edition {}", edition);
499             fmt_args.iter().for_each(|f| print!(" {}", f));
500             files.iter().for_each(|f| print!(" {}", f.display()));
501             println!();
502         }
503 
504         let mut command = rustfmt_command()
505             .stdout(stdout)
506             .args(files)
507             .args(&["--edition", edition])
508             .args(fmt_args)
509             .spawn()
510             .map_err(|e| match e.kind() {
511                 io::ErrorKind::NotFound => io::Error::new(
512                     io::ErrorKind::Other,
513                     "Could not run rustfmt, please make sure it is in your PATH.",
514                 ),
515                 _ => e,
516             })?;
517 
518         status.push(command.wait()?);
519     }
520 
521     Ok(status
522         .iter()
523         .filter_map(|s| if s.success() { None } else { s.code() })
524         .next()
525         .unwrap_or(SUCCESS))
526 }
527 
get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error>528 fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
529     let mut cmd = cargo_metadata::MetadataCommand::new();
530     cmd.no_deps();
531     if let Some(manifest_path) = manifest_path {
532         cmd.manifest_path(manifest_path);
533     }
534     cmd.other_options(vec![String::from("--offline")]);
535 
536     match cmd.exec() {
537         Ok(metadata) => Ok(metadata),
538         Err(_) => {
539             cmd.other_options(vec![]);
540             match cmd.exec() {
541                 Ok(metadata) => Ok(metadata),
542                 Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
543             }
544         }
545     }
546 }
547