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