1 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/).
7 use std::cmp::Ordering;
8 use std::collections::{BTreeMap, BTreeSet};
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;
18 use structopt::StructOpt;
20 #[derive(StructOpt, Debug)]
22 bin_name = "cargo fmt",
24 about = "This utility formats all bin and lib files of \
25 the current crate using rustfmt."
28 /// No output printed to stdout
29 #[structopt(short = "q", long = "quiet")]
32 /// Use verbose output
33 #[structopt(short = "v", long = "verbose")]
36 /// Print rustfmt version and exit
37 #[structopt(long = "version")]
40 /// Specify package to format (only usable in workspaces)
41 #[structopt(short = "p", long = "package", value_name = "package")]
42 packages: Vec<String>,
44 /// Specify path to Cargo.toml
45 #[structopt(long = "manifest-path", value_name = "manifest-path")]
46 manifest_path: Option<String>,
48 /// Options passed to rustfmt
49 // 'raw = true' to make `--` explicit.
50 #[structopt(name = "rustfmt_options", raw(raw = "true"))]
51 rustfmt_options: Vec<String>,
53 /// Format all packages (only usable in workspaces)
54 #[structopt(long = "all")]
59 let exit_status = execute();
60 std::io::stdout().flush().unwrap();
61 std::process::exit(exit_status);
64 const SUCCESS: i32 = 0;
65 const FAILURE: i32 = 1;
68 // Drop extra `fmt` argument provided by `cargo`.
69 let mut found_fmt = false;
70 let args = env::args().filter(|x| {
74 found_fmt = x == "fmt";
79 let opts = Opts::from_iter(args);
81 let verbosity = match (opts.verbose, opts.quiet) {
82 (false, false) => Verbosity::Normal,
83 (false, true) => Verbosity::Quiet,
84 (true, false) => Verbosity::Verbose,
86 print_usage_to_stderr("quiet mode and verbose mode are not compatible");
92 return handle_command_status(get_version());
95 let strategy = CargoFmtStrategy::from_opts(&opts);
97 if opts.manifest_path.is_some() {
98 let specified_manifest_path = opts.manifest_path.unwrap();
99 if !specified_manifest_path.ends_with("Cargo.toml") {
100 print_usage_to_stderr("the manifest-path must be a path to a Cargo.toml file");
103 let manifest_path = PathBuf::from(specified_manifest_path);
104 handle_command_status(format_crate(
107 opts.rustfmt_options,
108 Some(&manifest_path),
111 handle_command_status(format_crate(
114 opts.rustfmt_options,
120 fn print_usage_to_stderr(reason: &str) {
121 eprintln!("{}", reason);
122 let app = Opts::clap();
124 .write_help(&mut io::stderr())
125 .expect("failed to write to stderr");
128 #[derive(Debug, Clone, Copy, PartialEq)]
135 fn handle_command_status(status: Result<i32, io::Error>) -> i32 {
138 print_usage_to_stderr(&e.to_string());
141 Ok(status) => status,
145 fn get_version() -> Result<i32, io::Error> {
146 let mut command = Command::new("rustfmt")
147 .stdout(std::process::Stdio::inherit())
148 .args(&[String::from("--version")])
150 .map_err(|e| match e.kind() {
151 io::ErrorKind::NotFound => io::Error::new(
152 io::ErrorKind::Other,
153 "Could not run rustfmt, please make sure it is in your PATH.",
157 let result = command.wait()?;
158 if result.success() {
161 Ok(result.code().unwrap_or(SUCCESS))
166 verbosity: Verbosity,
167 strategy: &CargoFmtStrategy,
168 rustfmt_args: Vec<String>,
169 manifest_path: Option<&Path>,
170 ) -> Result<i32, io::Error> {
171 let targets = if rustfmt_args
173 .any(|s| ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str()))
177 get_targets(strategy, manifest_path)?
180 // Currently only bin and lib files get formatted.
181 run_rustfmt(&targets, &rustfmt_args, verbosity)
184 /// Target uses a `path` field for equality and hashing.
187 /// A path to the main source file of the target.
189 /// A kind of target (e.g., lib, bin, example, ...).
191 /// Rust edition for this target.
196 pub fn from_target(target: &cargo_metadata::Target) -> Self {
197 let path = PathBuf::from(&target.src_path);
198 let canonicalized = fs::canonicalize(&path).unwrap_or(path);
202 kind: target.kind[0].clone(),
203 edition: target.edition.clone(),
208 impl PartialEq for Target {
209 fn eq(&self, other: &Target) -> bool {
210 self.path == other.path
214 impl PartialOrd for Target {
215 fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
216 Some(self.path.cmp(&other.path))
220 impl Ord for Target {
221 fn cmp(&self, other: &Target) -> Ordering {
222 self.path.cmp(&other.path)
226 impl Eq for Target {}
228 impl Hash for Target {
229 fn hash<H: Hasher>(&self, state: &mut H) {
230 self.path.hash(state);
234 #[derive(Debug, PartialEq, Eq)]
235 pub enum CargoFmtStrategy {
236 /// Format every packages and dependencies.
238 /// Format packages that are specified by the command line argument.
240 /// Format the root packages only.
244 impl CargoFmtStrategy {
245 pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
246 match (opts.format_all, opts.packages.is_empty()) {
247 (false, true) => CargoFmtStrategy::Root,
248 (true, _) => CargoFmtStrategy::All,
249 (false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
254 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
256 strategy: &CargoFmtStrategy,
257 manifest_path: Option<&Path>,
258 ) -> Result<BTreeSet<Target>, io::Error> {
259 let mut targets = BTreeSet::new();
262 CargoFmtStrategy::Root => get_targets_root_only(manifest_path, &mut targets)?,
263 CargoFmtStrategy::All => {
264 get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())?
266 CargoFmtStrategy::Some(ref hitlist) => {
267 get_targets_with_hitlist(manifest_path, hitlist, &mut targets)?
271 if targets.is_empty() {
273 io::ErrorKind::Other,
274 "Failed to find targets".to_owned(),
281 fn get_targets_root_only(
282 manifest_path: Option<&Path>,
283 targets: &mut BTreeSet<Target>,
284 ) -> Result<(), io::Error> {
285 let metadata = get_cargo_metadata(manifest_path, false)?;
286 let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
287 let (in_workspace_root, current_dir_manifest) = if manifest_path.is_some() {
288 let target_manifest = manifest_path.unwrap().canonicalize()?;
289 (workspace_root_path == target_manifest, target_manifest)
291 let current_dir = env::current_dir()?.canonicalize()?;
293 workspace_root_path == current_dir,
294 current_dir.join("Cargo.toml"),
298 let package_targets = match metadata.packages.len() {
299 1 => metadata.packages.into_iter().next().unwrap().targets,
305 || PathBuf::from(&p.manifest_path)
308 == current_dir_manifest
315 for target in package_targets {
316 targets.insert(Target::from_target(&target));
322 fn get_targets_recursive(
323 manifest_path: Option<&Path>,
324 mut targets: &mut BTreeSet<Target>,
325 visited: &mut BTreeSet<String>,
326 ) -> Result<(), io::Error> {
327 let metadata = get_cargo_metadata(manifest_path, false)?;
328 let metadata_with_deps = get_cargo_metadata(manifest_path, true)?;
330 for package in metadata.packages {
331 add_targets(&package.targets, &mut targets);
333 // Look for local dependencies.
334 for dependency in package.dependencies {
335 if dependency.source.is_some() || visited.contains(&dependency.name) {
339 let dependency_package = metadata_with_deps
342 .find(|p| p.name == dependency.name);
343 let manifest_path = if dependency_package.is_some() {
344 PathBuf::from(&dependency_package.unwrap().manifest_path)
346 let mut package_manifest_path = PathBuf::from(&package.manifest_path);
347 package_manifest_path.pop();
348 package_manifest_path.push(&dependency.name);
349 package_manifest_path.push("Cargo.toml");
350 package_manifest_path
353 if manifest_path.exists() {
354 visited.insert(dependency.name);
355 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
363 fn get_targets_with_hitlist(
364 manifest_path: Option<&Path>,
366 targets: &mut BTreeSet<Target>,
367 ) -> Result<(), io::Error> {
368 let metadata = get_cargo_metadata(manifest_path, false)?;
370 let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
372 for package in metadata.packages {
373 if workspace_hitlist.remove(&package.name) {
374 for target in package.targets {
375 targets.insert(Target::from_target(&target));
380 if workspace_hitlist.is_empty() {
383 let package = workspace_hitlist.iter().next().unwrap();
385 io::ErrorKind::InvalidInput,
386 format!("package `{}` is not a member of the workspace", package),
391 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
392 for target in target_paths {
393 targets.insert(Target::from_target(target));
398 targets: &BTreeSet<Target>,
400 verbosity: Verbosity,
401 ) -> Result<i32, io::Error> {
402 let by_edition = targets
405 if verbosity == Verbosity::Verbose {
406 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
409 .fold(BTreeMap::new(), |mut h, t| {
410 h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
414 let mut status = vec![];
415 for (edition, files) in by_edition {
416 let stdout = if verbosity == Verbosity::Quiet {
417 std::process::Stdio::null()
419 std::process::Stdio::inherit()
422 if verbosity == Verbosity::Verbose {
424 print!(" --edition {}", edition);
425 fmt_args.iter().for_each(|f| print!(" {}", f));
426 files.iter().for_each(|f| print!(" {}", f.display()));
430 let mut command = Command::new("rustfmt")
433 .args(&["--edition", edition])
436 .map_err(|e| match e.kind() {
437 io::ErrorKind::NotFound => io::Error::new(
438 io::ErrorKind::Other,
439 "Could not run rustfmt, please make sure it is in your PATH.",
444 status.push(command.wait()?);
449 .filter_map(|s| if s.success() { None } else { s.code() })
454 fn get_cargo_metadata(
455 manifest_path: Option<&Path>,
457 ) -> Result<cargo_metadata::Metadata, io::Error> {
458 let mut cmd = cargo_metadata::MetadataCommand::new();
462 if let Some(manifest_path) = manifest_path {
463 cmd.manifest_path(manifest_path);
466 Ok(metadata) => Ok(metadata),
467 Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
472 mod cargo_fmt_tests {
476 fn default_options() {
477 let empty: Vec<String> = vec![];
478 let o = Opts::from_iter(&empty);
479 assert_eq!(false, o.quiet);
480 assert_eq!(false, o.verbose);
481 assert_eq!(false, o.version);
482 assert_eq!(empty, o.packages);
483 assert_eq!(empty, o.rustfmt_options);
484 assert_eq!(false, o.format_all);
489 let o = Opts::from_iter(&[
500 assert_eq!(true, o.quiet);
501 assert_eq!(false, o.verbose);
502 assert_eq!(false, o.version);
503 assert_eq!(vec!["p1", "p2"], o.packages);
504 assert_eq!(vec!["--edition", "2018"], o.rustfmt_options);
505 assert_eq!(false, o.format_all);
509 fn unexpected_option() {
512 .get_matches_from_safe(&["test", "unexpected"])
518 fn unexpected_flag() {
521 .get_matches_from_safe(&["test", "--flag"])
527 fn mandatory_separator() {
530 .get_matches_from_safe(&["test", "--check"])
535 .get_matches_from_safe(&["test", "--", "--check"])
541 fn multiple_packages_one_by_one() {
542 let o = Opts::from_iter(&[
551 assert_eq!(3, o.packages.len());
555 fn multiple_packages_grouped() {
556 let o = Opts::from_iter(&[
565 assert_eq!(4, o.packages.len());
569 fn empty_packages_1() {
570 assert!(Opts::clap().get_matches_from_safe(&["test", "-p"]).is_err());
574 fn empty_packages_2() {
577 .get_matches_from_safe(&["test", "-p", "--", "--check"])
583 fn empty_packages_3() {
586 .get_matches_from_safe(&["test", "-p", "--verbose"])
592 fn empty_packages_4() {
595 .get_matches_from_safe(&["test", "-p", "--check"])