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