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 /// Options passed to rustfmt
45 // 'raw = true' to make `--` explicit.
46 #[structopt(name = "rustfmt_options", raw(raw = "true"))]
47 rustfmt_options: Vec<String>,
49 /// Format all packages (only usable in workspaces)
50 #[structopt(long = "all")]
55 let exit_status = execute();
56 std::io::stdout().flush().unwrap();
57 std::process::exit(exit_status);
60 const SUCCESS: i32 = 0;
61 const FAILURE: i32 = 1;
64 // Drop extra `fmt` argument provided by `cargo`.
65 let mut found_fmt = false;
66 let args = env::args().filter(|x| {
70 found_fmt = x == "fmt";
75 let opts = Opts::from_iter(args);
77 let verbosity = match (opts.verbose, opts.quiet) {
78 (false, false) => Verbosity::Normal,
79 (false, true) => Verbosity::Quiet,
80 (true, false) => Verbosity::Verbose,
82 print_usage_to_stderr("quiet mode and verbose mode are not compatible");
88 return handle_command_status(get_version());
91 let strategy = CargoFmtStrategy::from_opts(&opts);
93 handle_command_status(format_crate(verbosity, &strategy, opts.rustfmt_options))
96 fn print_usage_to_stderr(reason: &str) {
97 eprintln!("{}", reason);
98 let app = Opts::clap();
99 app.write_help(&mut io::stderr())
100 .expect("failed to write to stderr");
103 #[derive(Debug, Clone, Copy, PartialEq)]
110 fn handle_command_status(status: Result<i32, io::Error>) -> i32 {
113 print_usage_to_stderr(&e.to_string());
116 Ok(status) => status,
120 fn get_version() -> Result<i32, io::Error> {
121 let mut command = Command::new("rustfmt")
122 .stdout(std::process::Stdio::inherit())
123 .args(&[String::from("--version")])
125 .map_err(|e| match e.kind() {
126 io::ErrorKind::NotFound => io::Error::new(
127 io::ErrorKind::Other,
128 "Could not run rustfmt, please make sure it is in your PATH.",
132 let result = command.wait()?;
133 if result.success() {
136 Ok(result.code().unwrap_or(SUCCESS))
141 verbosity: Verbosity,
142 strategy: &CargoFmtStrategy,
143 rustfmt_args: Vec<String>,
144 ) -> Result<i32, io::Error> {
145 let targets = if rustfmt_args
147 .any(|s| ["--print-config", "-h", "--help", "-V", "--version"].contains(&s.as_str()))
151 get_targets(strategy)?
154 // Currently only bin and lib files get formatted.
155 run_rustfmt(&targets, &rustfmt_args, verbosity)
158 /// Target uses a `path` field for equality and hashing.
161 /// A path to the main source file of the target.
163 /// A kind of target (e.g., lib, bin, example, ...).
165 /// Rust edition for this target.
170 pub fn from_target(target: &cargo_metadata::Target) -> Self {
171 let path = PathBuf::from(&target.src_path);
172 let canonicalized = fs::canonicalize(&path).unwrap_or(path);
176 kind: target.kind[0].clone(),
177 edition: target.edition.clone(),
182 impl PartialEq for Target {
183 fn eq(&self, other: &Target) -> bool {
184 self.path == other.path
188 impl PartialOrd for Target {
189 fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
190 Some(self.path.cmp(&other.path))
194 impl Ord for Target {
195 fn cmp(&self, other: &Target) -> Ordering {
196 self.path.cmp(&other.path)
200 impl Eq for Target {}
202 impl Hash for Target {
203 fn hash<H: Hasher>(&self, state: &mut H) {
204 self.path.hash(state);
208 #[derive(Debug, PartialEq, Eq)]
209 pub enum CargoFmtStrategy {
210 /// Format every packages and dependencies.
212 /// Format packages that are specified by the command line argument.
214 /// Format the root packages only.
218 impl CargoFmtStrategy {
219 pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
220 match (opts.format_all, opts.packages.is_empty()) {
221 (false, true) => CargoFmtStrategy::Root,
222 (true, _) => CargoFmtStrategy::All,
223 (false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
228 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
229 fn get_targets(strategy: &CargoFmtStrategy) -> Result<BTreeSet<Target>, io::Error> {
230 let mut targets = BTreeSet::new();
233 CargoFmtStrategy::Root => get_targets_root_only(&mut targets)?,
234 CargoFmtStrategy::All => get_targets_recursive(None, &mut targets, &mut BTreeSet::new())?,
235 CargoFmtStrategy::Some(ref hitlist) => get_targets_with_hitlist(hitlist, &mut targets)?,
238 if targets.is_empty() {
240 io::ErrorKind::Other,
241 "Failed to find targets".to_owned(),
248 fn get_targets_root_only(targets: &mut BTreeSet<Target>) -> Result<(), io::Error> {
249 let metadata = get_cargo_metadata(None)?;
250 let current_dir = env::current_dir()?.canonicalize()?;
251 let current_dir_manifest = current_dir.join("Cargo.toml");
252 let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?;
253 let in_workspace_root = workspace_root_path == current_dir;
255 for package in metadata.packages {
256 if in_workspace_root || PathBuf::from(&package.manifest_path) == current_dir_manifest {
257 for target in package.targets {
258 targets.insert(Target::from_target(&target));
266 fn get_targets_recursive(
267 manifest_path: Option<&Path>,
268 mut targets: &mut BTreeSet<Target>,
269 visited: &mut BTreeSet<String>,
270 ) -> Result<(), io::Error> {
271 let metadata = get_cargo_metadata(manifest_path)?;
273 for package in metadata.packages {
274 add_targets(&package.targets, &mut targets);
276 // Look for local dependencies.
277 for dependency in package.dependencies {
278 if dependency.source.is_some() || visited.contains(&dependency.name) {
282 let mut manifest_path = PathBuf::from(&package.manifest_path);
285 manifest_path.push(&dependency.name);
286 manifest_path.push("Cargo.toml");
288 if manifest_path.exists() {
289 visited.insert(dependency.name);
290 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
298 fn get_targets_with_hitlist(
300 targets: &mut BTreeSet<Target>,
301 ) -> Result<(), io::Error> {
302 let metadata = get_cargo_metadata(None)?;
304 let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(hitlist);
306 for package in metadata.packages {
307 if workspace_hitlist.remove(&package.name) {
308 for target in package.targets {
309 targets.insert(Target::from_target(&target));
314 if workspace_hitlist.is_empty() {
317 let package = workspace_hitlist.iter().next().unwrap();
319 io::ErrorKind::InvalidInput,
320 format!("package `{}` is not a member of the workspace", package),
325 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
326 for target in target_paths {
327 targets.insert(Target::from_target(target));
332 targets: &BTreeSet<Target>,
334 verbosity: Verbosity,
335 ) -> Result<i32, io::Error> {
336 let by_edition = targets
339 if verbosity == Verbosity::Verbose {
340 println!("[{} ({})] {:?}", t.kind, t.edition, t.path)
343 .fold(BTreeMap::new(), |mut h, t| {
344 h.entry(&t.edition).or_insert_with(Vec::new).push(&t.path);
348 let mut status = vec![];
349 for (edition, files) in by_edition {
350 let stdout = if verbosity == Verbosity::Quiet {
351 std::process::Stdio::null()
353 std::process::Stdio::inherit()
356 if verbosity == Verbosity::Verbose {
358 print!(" --edition {}", edition);
359 fmt_args.iter().for_each(|f| print!(" {}", f));
360 files.iter().for_each(|f| print!(" {}", f.display()));
364 let mut command = Command::new("rustfmt")
367 .args(&["--edition", edition])
370 .map_err(|e| match e.kind() {
371 io::ErrorKind::NotFound => io::Error::new(
372 io::ErrorKind::Other,
373 "Could not run rustfmt, please make sure it is in your PATH.",
378 status.push(command.wait()?);
383 .filter_map(|s| if s.success() { None } else { s.code() })
388 fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
389 let mut cmd = cargo_metadata::MetadataCommand::new();
391 if let Some(manifest_path) = manifest_path {
392 cmd.manifest_path(manifest_path);
395 Ok(metadata) => Ok(metadata),
396 Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
401 mod cargo_fmt_tests {
405 fn default_options() {
406 let empty: Vec<String> = vec![];
407 let o = Opts::from_iter(&empty);
408 assert_eq!(false, o.quiet);
409 assert_eq!(false, o.verbose);
410 assert_eq!(false, o.version);
411 assert_eq!(empty, o.packages);
412 assert_eq!(empty, o.rustfmt_options);
413 assert_eq!(false, o.format_all);
418 let o = Opts::from_iter(&[
429 assert_eq!(true, o.quiet);
430 assert_eq!(false, o.verbose);
431 assert_eq!(false, o.version);
432 assert_eq!(vec!["p1", "p2"], o.packages);
433 assert_eq!(vec!["--edition", "2018"], o.rustfmt_options);
434 assert_eq!(false, o.format_all);
438 fn unexpected_option() {
441 .get_matches_from_safe(&["test", "unexpected"])
447 fn unexpected_flag() {
450 .get_matches_from_safe(&["test", "--flag"])
456 fn mandatory_separator() {
459 .get_matches_from_safe(&["test", "--check"])
464 .get_matches_from_safe(&["test", "--", "--check"])
470 fn multiple_packages_one_by_one() {
471 let o = Opts::from_iter(&[
480 assert_eq!(3, o.packages.len());
484 fn multiple_packages_grouped() {
485 let o = Opts::from_iter(&[
494 assert_eq!(4, o.packages.len());
498 fn empty_packages_1() {
499 assert!(Opts::clap().get_matches_from_safe(&["test", "-p"]).is_err());
503 fn empty_packages_2() {
506 .get_matches_from_safe(&["test", "-p", "--", "--check"])
512 fn empty_packages_3() {
515 .get_matches_from_safe(&["test", "-p", "--verbose"])
521 fn empty_packages_4() {
524 .get_matches_from_safe(&["test", "-p", "--check"])