]> git.lizzy.rs Git - rust.git/blob - src/bin/cargo-fmt.rs
Merge pull request #2288 from davidalber/fix-2078
[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("", "all", "format all packages (only usable in workspaces)");
53
54     // If there is any invalid argument passed to `cargo fmt`, return without formatting.
55     let mut is_package_arg = false;
56     for arg in env::args().skip(2).take_while(|a| a != "--") {
57         if arg.starts_with('-') {
58             is_package_arg = arg.starts_with("--package");
59         } else if !is_package_arg {
60             print_usage_to_stderr(&opts, &format!("Invalid argument: `{}`.", arg));
61             return failure;
62         } else {
63             is_package_arg = false;
64         }
65     }
66
67     let matches = match opts.parse(env::args().skip(1).take_while(|a| a != "--")) {
68         Ok(m) => m,
69         Err(e) => {
70             print_usage_to_stderr(&opts, &e.to_string());
71             return failure;
72         }
73     };
74
75     let verbosity = match (matches.opt_present("v"), matches.opt_present("q")) {
76         (false, false) => Verbosity::Normal,
77         (false, true) => Verbosity::Quiet,
78         (true, false) => Verbosity::Verbose,
79         (true, true) => {
80             print_usage_to_stderr(&opts, "quiet mode and verbose mode are not compatible");
81             return failure;
82         }
83     };
84
85     if matches.opt_present("h") {
86         print_usage_to_stdout(&opts, "");
87         return success;
88     }
89
90     let strategy = CargoFmtStrategy::from_matches(&matches);
91
92     match format_crate(verbosity, &strategy) {
93         Err(e) => {
94             print_usage_to_stderr(&opts, &e.to_string());
95             failure
96         }
97         Ok(status) => {
98             if status.success() {
99                 success
100             } else {
101                 status.code().unwrap_or(failure)
102             }
103         }
104     }
105 }
106
107 macro_rules! print_usage {
108     ($print:ident, $opts:ident, $reason:expr) => ({
109         let msg = format!("{}\nusage: cargo fmt [options]", $reason);
110         $print!(
111             "{}\nThis utility formats all bin and lib files of the current crate using rustfmt. \
112              Arguments after `--` are passed to rustfmt.",
113             $opts.usage(&msg)
114         );
115     })
116 }
117
118 fn print_usage_to_stdout(opts: &Options, reason: &str) {
119     print_usage!(println, opts, reason);
120 }
121
122 fn print_usage_to_stderr(opts: &Options, reason: &str) {
123     print_usage!(eprintln, opts, reason);
124 }
125
126 #[derive(Debug, Clone, Copy, PartialEq)]
127 pub enum Verbosity {
128     Verbose,
129     Normal,
130     Quiet,
131 }
132
133 fn format_crate(
134     verbosity: Verbosity,
135     strategy: &CargoFmtStrategy,
136 ) -> Result<ExitStatus, io::Error> {
137     let rustfmt_args = get_fmt_args();
138     let targets = if rustfmt_args.iter().any(|s| s == "--dump-default-config") {
139         HashSet::new()
140     } else {
141         get_targets(strategy)?
142     };
143
144     // Currently only bin and lib files get formatted
145     let files: Vec<_> = targets
146         .into_iter()
147         .inspect(|t| {
148             if verbosity == Verbosity::Verbose {
149                 println!("[{}] {:?}", t.kind, t.path)
150             }
151         })
152         .map(|t| t.path)
153         .collect();
154
155     format_files(&files, &rustfmt_args, verbosity)
156 }
157
158 fn get_fmt_args() -> Vec<String> {
159     // All arguments after -- are passed to rustfmt
160     env::args().skip_while(|a| a != "--").skip(1).collect()
161 }
162
163 /// Target uses a `path` field for equality and hashing.
164 #[derive(Debug)]
165 pub struct Target {
166     /// A path to the main source file of the target.
167     path: PathBuf,
168     /// A kind of target (e.g. lib, bin, example, ...).
169     kind: String,
170 }
171
172 impl Target {
173     pub fn from_target(target: &cargo_metadata::Target) -> Self {
174         let path = PathBuf::from(&target.src_path);
175         let canonicalized = fs::canonicalize(&path).unwrap_or(path);
176
177         Target {
178             path: canonicalized,
179             kind: target.kind[0].clone(),
180         }
181     }
182 }
183
184 impl PartialEq for Target {
185     fn eq(&self, other: &Target) -> bool {
186         self.path == other.path
187     }
188 }
189
190 impl Eq for Target {}
191
192 impl Hash for Target {
193     fn hash<H: Hasher>(&self, state: &mut H) {
194         self.path.hash(state);
195     }
196 }
197
198 #[derive(Debug, PartialEq, Eq)]
199 pub enum CargoFmtStrategy {
200     /// Format every packages and dependencies.
201     All,
202     /// Format pacakges that are specified by the command line argument.
203     Some(Vec<String>),
204     /// Format the root packages only.
205     Root,
206 }
207
208 impl CargoFmtStrategy {
209     pub fn from_matches(matches: &Matches) -> CargoFmtStrategy {
210         match (matches.opt_present("all"), matches.opt_present("p")) {
211             (false, false) => CargoFmtStrategy::Root,
212             (true, _) => CargoFmtStrategy::All,
213             (false, true) => CargoFmtStrategy::Some(matches.opt_strs("p")),
214         }
215     }
216 }
217
218 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
219 fn get_targets(strategy: &CargoFmtStrategy) -> Result<HashSet<Target>, io::Error> {
220     let mut targets = HashSet::new();
221
222     match *strategy {
223         CargoFmtStrategy::Root => get_targets_root_only(&mut targets)?,
224         CargoFmtStrategy::All => get_targets_recursive(None, &mut targets, &mut HashSet::new())?,
225         CargoFmtStrategy::Some(ref hitlist) => get_targets_with_hitlist(hitlist, &mut targets)?,
226     }
227
228     if targets.is_empty() {
229         Err(io::Error::new(
230             io::ErrorKind::Other,
231             "Failed to find targets".to_owned(),
232         ))
233     } else {
234         Ok(targets)
235     }
236 }
237
238 fn get_targets_root_only(targets: &mut HashSet<Target>) -> Result<(), io::Error> {
239     let metadata = get_cargo_metadata(None)?;
240
241     for package in metadata.packages {
242         for target in package.targets {
243             if is_target_workspace_members(&target.name, &metadata.workspace_members) {
244                 targets.insert(Target::from_target(&target));
245             }
246         }
247     }
248
249     Ok(())
250 }
251
252 fn is_target_workspace_members(target: &str, workspace_members: &[String]) -> bool {
253     workspace_members.iter().any(|member| {
254         member
255             .split_whitespace()
256             .nth(0)
257             .map_or(false, |name| name == target)
258     })
259 }
260
261 fn get_targets_recursive(
262     manifest_path: Option<&Path>,
263     mut targets: &mut HashSet<Target>,
264     visited: &mut HashSet<String>,
265 ) -> Result<(), io::Error> {
266     let metadata = get_cargo_metadata(manifest_path)?;
267
268     for package in metadata.packages {
269         add_targets(&package.targets, &mut targets);
270
271         // Look for local dependencies.
272         for dependency in package.dependencies {
273             if dependency.source.is_some() || visited.contains(&dependency.name) {
274                 continue;
275             }
276
277             let mut manifest_path = PathBuf::from(&package.manifest_path);
278
279             manifest_path.pop();
280             manifest_path.push(&dependency.name);
281             manifest_path.push("Cargo.toml");
282
283             if manifest_path.exists() {
284                 visited.insert(dependency.name);
285                 get_targets_recursive(Some(&manifest_path), &mut targets, visited)?;
286             }
287         }
288     }
289
290     Ok(())
291 }
292
293 fn get_targets_with_hitlist(
294     hitlist: &[String],
295     targets: &mut HashSet<Target>,
296 ) -> Result<(), io::Error> {
297     let metadata = get_cargo_metadata(None)?;
298
299     let mut workspace_hitlist: HashSet<&String> = HashSet::from_iter(hitlist);
300
301     for package in metadata.packages {
302         if workspace_hitlist.remove(&package.name) {
303             for target in package.targets {
304                 targets.insert(Target::from_target(&target));
305             }
306         }
307     }
308
309     if workspace_hitlist.is_empty() {
310         Ok(())
311     } else {
312         let package = workspace_hitlist.iter().next().unwrap();
313         Err(io::Error::new(
314             io::ErrorKind::InvalidInput,
315             format!("package `{}` is not a member of the workspace", package),
316         ))
317     }
318 }
319
320 fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut HashSet<Target>) {
321     for target in target_paths {
322         targets.insert(Target::from_target(target));
323     }
324 }
325
326 fn format_files(
327     files: &[PathBuf],
328     fmt_args: &[String],
329     verbosity: Verbosity,
330 ) -> Result<ExitStatus, io::Error> {
331     let stdout = if verbosity == Verbosity::Quiet {
332         std::process::Stdio::null()
333     } else {
334         std::process::Stdio::inherit()
335     };
336
337     if verbosity == Verbosity::Verbose {
338         print!("rustfmt");
339         for a in fmt_args {
340             print!(" {}", a);
341         }
342         for f in files {
343             print!(" {}", f.display());
344         }
345         println!();
346     }
347
348     let mut command = Command::new("rustfmt")
349         .stdout(stdout)
350         .args(files)
351         .args(fmt_args)
352         .spawn()
353         .map_err(|e| match e.kind() {
354             io::ErrorKind::NotFound => io::Error::new(
355                 io::ErrorKind::Other,
356                 "Could not run rustfmt, please make sure it is in your PATH.",
357             ),
358             _ => e,
359         })?;
360
361     command.wait()
362 }
363
364 fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
365     match cargo_metadata::metadata(manifest_path) {
366         Ok(metadata) => Ok(metadata),
367         Err(..) => Err(io::Error::new(
368             io::ErrorKind::Other,
369             "`cargo manifest` failed.",
370         )),
371     }
372 }