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