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