]> git.proxmox.com Git - rustc.git/blob - src/tools/rustfmt/src/cargo-fmt/main.rs
759b21218c353e70787f853a6c09725bf7132b10
[rustc.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 #[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
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
78 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
146 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
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
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
220 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
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
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 {
274 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 {
287 fn eq(&self, other: &Target) -> bool {
288 self.path == other.path
289 }
290 }
291
292 impl PartialOrd for Target {
293 fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
294 Some(self.path.cmp(&other.path))
295 }
296 }
297
298 impl Ord for Target {
299 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 {
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 {
323 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.
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
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
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
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
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
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
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 }