]> git.lizzy.rs Git - rust.git/blob - src/bin/cargo-fmt.rs
Adding --version option to cargo-fmt
[rust.git] / src / bin / cargo-fmt.rs
1 // Copyright 2015-2016 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/)
12
13 #![cfg(not(test))]
14 #![deny(warnings)]
15
16 extern crate cargo_metadata;
17 extern crate getopts;
18 extern crate serde_json as json;
19
20 use std::env;
21 use std::fs;
22 use std::hash::{Hash, Hasher};
23 use std::io::{self, Write};
24 use std::path::{Path, PathBuf};
25 use std::process::{Command, ExitStatus};
26 use std::str;
27 use std::collections::HashSet;
28 use std::iter::FromIterator;
29
30 use getopts::{Matches, Options};
31
32 fn main() {
33     let exit_status = execute();
34     std::io::stdout().flush().unwrap();
35     std::process::exit(exit_status);
36 }
37
38 fn execute() -> i32 {
39     let success = 0;
40     let failure = 1;
41
42     let mut opts = getopts::Options::new();
43     opts.optflag("h", "help", "show this message");
44     opts.optflag("q", "quiet", "no output printed to stdout");
45     opts.optflag("v", "verbose", "use verbose output");
46     opts.optmulti(
47         "p",
48         "package",
49         "specify package to format (only usable in workspaces)",
50         "<package>",
51     );
52     opts.optflag("", "version", "print rustfmt version and exit");
53     opts.optflag("", "all", "format all packages (only usable in workspaces)");
54
55     // If there is any invalid argument passed to `cargo fmt`, return without formatting.
56     let mut is_package_arg = false;
57     for arg in env::args().skip(2).take_while(|a| a != "--") {
58         if arg.starts_with('-') {
59             is_package_arg = arg.starts_with("--package");
60         } else if !is_package_arg {
61             print_usage_to_stderr(&opts, &format!("Invalid argument: `{}`.", arg));
62             return failure;
63         } else {
64             is_package_arg = false;
65         }
66     }
67
68     let matches = match opts.parse(env::args().skip(1).take_while(|a| a != "--")) {
69         Ok(m) => m,
70         Err(e) => {
71             print_usage_to_stderr(&opts, &e.to_string());
72             return failure;
73         }
74     };
75
76     let verbosity = match (matches.opt_present("v"), matches.opt_present("q")) {
77         (false, false) => Verbosity::Normal,
78         (false, true) => Verbosity::Quiet,
79         (true, false) => Verbosity::Verbose,
80         (true, true) => {
81             print_usage_to_stderr(&opts, "quiet mode and verbose mode are not compatible");
82             return failure;
83         }
84     };
85
86     if matches.opt_present("h") {
87         print_usage_to_stdout(&opts, "");
88         return success;
89     }
90
91     if matches.opt_present("version") {
92         return handle_command_status(get_version(), &opts);
93     }
94
95     let strategy = CargoFmtStrategy::from_matches(&matches);
96
97     match format_crate(verbosity, &strategy) {
98         Err(e) => {
99             print_usage_to_stderr(&opts, &e.to_string());
100             failure
101         }
102         Ok(status) => {
103             if status.success() {
104                 success
105             } else {
106                 status.code().unwrap_or(failure)
107             }
108         }
109     }
110 }
111
112 macro_rules! print_usage {
113     ($print:ident, $opts:ident, $reason:expr) => ({
114         let msg = format!("{}\nusage: cargo fmt [options]", $reason);
115         $print!(
116             "{}\nThis utility formats all bin and lib files of the current crate using rustfmt. \
117              Arguments after `--` are passed to rustfmt.",
118             $opts.usage(&msg)
119         );
120     })
121 }
122
123 fn print_usage_to_stdout(opts: &Options, reason: &str) {
124     print_usage!(println, opts, reason);
125 }
126
127 fn print_usage_to_stderr(opts: &Options, reason: &str) {
128     print_usage!(eprintln, opts, reason);
129 }
130
131 #[derive(Debug, Clone, Copy, PartialEq)]
132 pub enum Verbosity {
133     Verbose,
134     Normal,
135     Quiet,
136 }
137
138 fn handle_command_status(status: Result<ExitStatus, io::Error>, opts: &getopts::Options) -> i32 {
139     let success = 0;
140     let failure = 1;
141
142     match status {
143         Err(e) => {
144             print_usage_to_stderr(&opts, &e.to_string());
145             failure
146         }
147         Ok(status) => {
148             if status.success() {
149                 success
150             } else {
151                 status.code().unwrap_or(failure)
152             }
153         }
154     }
155 }
156
157 fn get_version() -> Result<ExitStatus, io::Error> {
158     let mut command = Command::new("rustfmt")
159         .args(vec!["--version"])
160         .spawn()
161         .map_err(|e| match e.kind() {
162             io::ErrorKind::NotFound => io::Error::new(
163                 io::ErrorKind::Other,
164                 "Could not run rustfmt, please make sure it is in your PATH.",
165             ),
166             _ => e,
167         })?;
168
169     command.wait()
170 }
171
172 fn format_crate(
173     verbosity: Verbosity,
174     strategy: &CargoFmtStrategy,
175 ) -> Result<ExitStatus, io::Error> {
176     let rustfmt_args = get_fmt_args();
177     let targets = if rustfmt_args.iter().any(|s| s == "--dump-default-config") {
178         HashSet::new()
179     } else {
180         get_targets(strategy)?
181     };
182
183     // Currently only bin and lib files get formatted
184     let files: Vec<_> = targets
185         .into_iter()
186         .inspect(|t| {
187             if verbosity == Verbosity::Verbose {
188                 println!("[{}] {:?}", t.kind, t.path)
189             }
190         })
191         .map(|t| t.path)
192         .collect();
193
194     format_files(&files, &rustfmt_args, verbosity)
195 }
196
197 fn get_fmt_args() -> Vec<String> {
198     // All arguments after -- are passed to rustfmt
199     env::args().skip_while(|a| a != "--").skip(1).collect()
200 }
201
202 /// Target uses a `path` field for equality and hashing.
203 #[derive(Debug)]
204 pub struct Target {
205     /// A path to the main source file of the target.
206     path: PathBuf,
207     /// A kind of target (e.g. lib, bin, example, ...).
208     kind: String,
209 }
210
211 impl Target {
212     pub fn from_target(target: &cargo_metadata::Target) -> Self {
213         let path = PathBuf::from(&target.src_path);
214         let canonicalized = fs::canonicalize(&path).unwrap_or(path);
215
216         Target {
217             path: canonicalized,
218             kind: target.kind[0].clone(),
219         }
220     }
221 }
222
223 impl PartialEq for Target {
224     fn eq(&self, other: &Target) -> bool {
225         self.path == other.path
226     }
227 }
228
229 impl Eq for Target {}
230
231 impl Hash for Target {
232     fn hash<H: Hasher>(&self, state: &mut H) {
233         self.path.hash(state);
234     }
235 }
236
237 #[derive(Debug, PartialEq, Eq)]
238 pub enum CargoFmtStrategy {
239     /// Format every packages and dependencies.
240     All,
241     /// Format pacakges that are specified by the command line argument.
242     Some(Vec<String>),
243     /// Format the root packages only.
244     Root,
245 }
246
247 impl CargoFmtStrategy {
248     pub fn from_matches(matches: &Matches) -> CargoFmtStrategy {
249         match (matches.opt_present("all"), matches.opt_present("p")) {
250             (false, false) => CargoFmtStrategy::Root,
251             (true, _) => CargoFmtStrategy::All,
252             (false, true) => CargoFmtStrategy::Some(matches.opt_strs("p")),
253         }
254     }
255 }
256
257 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
258 fn get_targets(strategy: &CargoFmtStrategy) -> Result<HashSet<Target>, io::Error> {
259     let mut targets = HashSet::new();
260
261     match *strategy {
262         CargoFmtStrategy::Root => get_targets_root_only(&mut targets)?,
263         CargoFmtStrategy::All => get_targets_recursive(None, &mut targets, &mut HashSet::new())?,
264         CargoFmtStrategy::Some(ref hitlist) => get_targets_with_hitlist(hitlist, &mut targets)?,
265     }
266
267     if targets.is_empty() {
268         Err(io::Error::new(
269             io::ErrorKind::Other,
270             "Failed to find targets".to_owned(),
271         ))
272     } else {
273         Ok(targets)
274     }
275 }
276
277 fn get_targets_root_only(targets: &mut HashSet<Target>) -> Result<(), io::Error> {
278     let metadata = get_cargo_metadata(None)?;
279
280     for package in metadata.packages {
281         for target in package.targets {
282             if target.name == package.name {
283                 targets.insert(Target::from_target(&target));
284             }
285         }
286     }
287
288     Ok(())
289 }
290
291 fn get_targets_recursive(
292     manifest_path: Option<&Path>,
293     mut targets: &mut HashSet<Target>,
294     visited: &mut HashSet<String>,
295 ) -> Result<(), io::Error> {
296     let metadata = get_cargo_metadata(manifest_path)?;
297
298     for package in metadata.packages {
299         add_targets(&package.targets, &mut targets);
300
301         // Look for local dependencies.
302         for dependency in package.dependencies {
303             if dependency.source.is_some() || visited.contains(&dependency.name) {
304                 continue;
305             }
306
307             let mut manifest_path = PathBuf::from(&package.manifest_path);
308
309             manifest_path.pop();
310             manifest_path.push(&dependency.name);
311             manifest_path.push("Cargo.toml");
312
313             if manifest_path.exists() {
314                 visited.insert(dependency.name);
315                 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
316             }
317         }
318     }
319
320     Ok(())
321 }
322
323 fn get_targets_with_hitlist(
324     hitlist: &[String],
325     targets: &mut HashSet<Target>,
326 ) -> Result<(), io::Error> {
327     let metadata = get_cargo_metadata(None)?;
328
329     let mut workspace_hitlist: HashSet<&String> = HashSet::from_iter(hitlist);
330
331     for package in metadata.packages {
332         if workspace_hitlist.remove(&package.name) {
333             for target in package.targets {
334                 targets.insert(Target::from_target(&target));
335             }
336         }
337     }
338
339     if workspace_hitlist.is_empty() {
340         Ok(())
341     } else {
342         let package = workspace_hitlist.iter().next().unwrap();
343         Err(io::Error::new(
344             io::ErrorKind::InvalidInput,
345             format!("package `{}` is not a member of the workspace", package),
346         ))
347     }
348 }
349
350 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut HashSet<Target>) {
351     for target in target_paths {
352         targets.insert(Target::from_target(target));
353     }
354 }
355
356 fn format_files(
357     files: &[PathBuf],
358     fmt_args: &[String],
359     verbosity: Verbosity,
360 ) -> Result<ExitStatus, io::Error> {
361     let stdout = if verbosity == Verbosity::Quiet {
362         std::process::Stdio::null()
363     } else {
364         std::process::Stdio::inherit()
365     };
366
367     if verbosity == Verbosity::Verbose {
368         print!("rustfmt");
369         for a in fmt_args {
370             print!(" {}", a);
371         }
372         for f in files {
373             print!(" {}", f.display());
374         }
375         println!();
376     }
377
378     let mut command = Command::new("rustfmt")
379         .stdout(stdout)
380         .args(files)
381         .args(fmt_args)
382         .spawn()
383         .map_err(|e| match e.kind() {
384             io::ErrorKind::NotFound => io::Error::new(
385                 io::ErrorKind::Other,
386                 "Could not run rustfmt, please make sure it is in your PATH.",
387             ),
388             _ => e,
389         })?;
390
391     command.wait()
392 }
393
394 fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
395     match cargo_metadata::metadata(manifest_path) {
396         Ok(metadata) => Ok(metadata),
397         Err(..) => Err(io::Error::new(
398             io::ErrorKind::Other,
399             "`cargo manifest` failed.",
400         )),
401     }
402 }