]> git.lizzy.rs Git - rust.git/blob - src/bin/cargo-fmt.rs
Format source codes
[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 getopts;
17 extern crate serde_json as json;
18
19 use std::env;
20 use std::io::Write;
21 use std::path::PathBuf;
22 use std::process::{Command, ExitStatus};
23 use std::str;
24 use std::collections::HashSet;
25 use std::iter::FromIterator;
26
27 use json::Value;
28
29 use getopts::{Options, Matches};
30
31 fn main() {
32     let exit_status = execute();
33     std::io::stdout().flush().unwrap();
34     std::process::exit(exit_status);
35 }
36
37 fn execute() -> i32 {
38     let success = 0;
39     let failure = 1;
40
41     let mut opts = getopts::Options::new();
42     opts.optflag("h", "help", "show this message");
43     opts.optflag("q", "quiet", "no output printed to stdout");
44     opts.optflag("v", "verbose", "use verbose output");
45     opts.optmulti(
46         "p",
47         "package",
48         "specify package to format (only usable in workspaces)",
49         "<package>",
50     );
51     opts.optflag("", "all", "format all packages (only usable in workspaces)");
52
53     // If there is any invalid argument passed to `cargo fmt`, return without formatting.
54     if let Some(arg) = env::args()
55         .skip(2)
56         .take_while(|a| a != "--")
57         .find(|a| !a.starts_with('-'))
58     {
59         print_usage(&opts, &format!("Invalid argument: `{}`.", arg));
60         return failure;
61     }
62
63     let matches = match opts.parse(env::args().skip(1).take_while(|a| a != "--")) {
64         Ok(m) => m,
65         Err(e) => {
66             print_usage(&opts, &e.to_string());
67             return failure;
68         }
69     };
70
71     let verbosity = match (matches.opt_present("v"), matches.opt_present("q")) {
72         (false, false) => Verbosity::Normal,
73         (false, true) => Verbosity::Quiet,
74         (true, false) => Verbosity::Verbose,
75         (true, true) => {
76             print_usage(&opts, "quiet mode and verbose mode are not compatible");
77             return failure;
78         }
79     };
80
81     if matches.opt_present("h") {
82         print_usage(&opts, "");
83         return success;
84     }
85
86     let workspace_hitlist = WorkspaceHitlist::from_matches(&matches);
87
88     match format_crate(verbosity, workspace_hitlist) {
89         Err(e) => {
90             print_usage(&opts, &e.to_string());
91             failure
92         }
93         Ok(status) => {
94             if status.success() {
95                 success
96             } else {
97                 status.code().unwrap_or(failure)
98             }
99         }
100     }
101 }
102
103 fn print_usage(opts: &Options, reason: &str) {
104     let msg = format!("{}\nusage: cargo fmt [options]", reason);
105     println!(
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 #[derive(Debug, Clone, Copy, PartialEq)]
113 pub enum Verbosity {
114     Verbose,
115     Normal,
116     Quiet,
117 }
118
119 fn format_crate(
120     verbosity: Verbosity,
121     workspace_hitlist: WorkspaceHitlist,
122 ) -> Result<ExitStatus, std::io::Error> {
123     let targets = get_targets(workspace_hitlist)?;
124
125     // Currently only bin and lib files get formatted
126     let files: Vec<_> = targets
127         .into_iter()
128         .filter(|t| t.kind.should_format())
129         .inspect(|t| if verbosity == Verbosity::Verbose {
130             println!("[{:?}] {:?}", t.kind, t.path)
131         })
132         .map(|t| t.path)
133         .collect();
134
135     format_files(&files, &get_fmt_args(), verbosity)
136 }
137
138 fn get_fmt_args() -> Vec<String> {
139     // All arguments after -- are passed to rustfmt
140     env::args().skip_while(|a| a != "--").skip(1).collect()
141 }
142
143 #[derive(Debug)]
144 enum TargetKind {
145     Lib,         // dylib, staticlib, lib
146     Bin,         // bin
147     Example,     // example file
148     Test,        // test file
149     Bench,       // bench file
150     CustomBuild, // build script
151     ProcMacro,   // a proc macro implementation
152     Other,       // plugin,...
153 }
154
155 impl TargetKind {
156     fn should_format(&self) -> bool {
157         match *self {
158             TargetKind::Lib |
159             TargetKind::Bin |
160             TargetKind::Example |
161             TargetKind::Test |
162             TargetKind::Bench |
163             TargetKind::CustomBuild |
164             TargetKind::ProcMacro => true,
165             _ => false,
166         }
167     }
168 }
169
170 #[derive(Debug)]
171 pub struct Target {
172     path: PathBuf,
173     kind: TargetKind,
174 }
175
176 #[derive(Debug, PartialEq, Eq)]
177 pub enum WorkspaceHitlist {
178     All,
179     Some(Vec<String>),
180     None,
181 }
182
183 impl WorkspaceHitlist {
184     pub fn get_some<'a>(&'a self) -> Option<&'a [String]> {
185         if let &WorkspaceHitlist::Some(ref hitlist) = self {
186             Some(&hitlist)
187         } else {
188             None
189         }
190     }
191
192     pub fn from_matches(matches: &Matches) -> WorkspaceHitlist {
193         match (matches.opt_present("all"), matches.opt_present("p")) {
194             (false, false) => WorkspaceHitlist::None,
195             (true, _) => WorkspaceHitlist::All,
196             (false, true) => WorkspaceHitlist::Some(matches.opt_strs("p")),
197         }
198     }
199 }
200
201 // Returns a vector of all compile targets of a crate
202 fn get_targets(workspace_hitlist: WorkspaceHitlist) -> Result<Vec<Target>, std::io::Error> {
203     let mut targets: Vec<Target> = vec![];
204     if workspace_hitlist == WorkspaceHitlist::None {
205         let output = Command::new("cargo").arg("read-manifest").output()?;
206         if output.status.success() {
207             // None of the unwraps should fail if output of `cargo read-manifest` is correct
208             let data = &String::from_utf8(output.stdout).unwrap();
209             let json: Value = json::from_str(data).unwrap();
210             let json_obj = json.as_object().unwrap();
211             let jtargets = json_obj.get("targets").unwrap().as_array().unwrap();
212             for jtarget in jtargets {
213                 targets.push(target_from_json(jtarget));
214             }
215
216             return Ok(targets);
217         }
218         return Err(std::io::Error::new(
219             std::io::ErrorKind::NotFound,
220             str::from_utf8(&output.stderr).unwrap(),
221         ));
222     }
223     // This happens when cargo-fmt is not used inside a crate or
224     // is used inside a workspace.
225     // To ensure backward compatability, we only use `cargo metadata` for workspaces.
226     // TODO: Is it possible only use metadata or read-manifest
227     let output = Command::new("cargo")
228         .arg("metadata")
229         .arg("--no-deps")
230         .output()?;
231     if output.status.success() {
232         let data = &String::from_utf8(output.stdout).unwrap();
233         let json: Value = json::from_str(data).unwrap();
234         let json_obj = json.as_object().unwrap();
235         let mut hitlist: HashSet<&String> = if workspace_hitlist != WorkspaceHitlist::All {
236             HashSet::from_iter(workspace_hitlist.get_some().unwrap())
237         } else {
238             HashSet::new() // Unused
239         };
240         let members: Vec<&Value> = json_obj
241             .get("packages")
242             .unwrap()
243             .as_array()
244             .unwrap()
245             .into_iter()
246             .filter(|member| if workspace_hitlist == WorkspaceHitlist::All {
247                 true
248             } else {
249                 let member_obj = member.as_object().unwrap();
250                 let member_name = member_obj.get("name").unwrap().as_str().unwrap();
251                 hitlist.take(&member_name.to_string()).is_some()
252             })
253             .collect();
254         if hitlist.len() != 0 {
255             // Mimick cargo of only outputting one <package> spec.
256             return Err(std::io::Error::new(
257                 std::io::ErrorKind::InvalidInput,
258                 format!(
259                     "package `{}` is not a member of the workspace",
260                     hitlist.iter().next().unwrap()
261                 ),
262             ));
263         }
264         for member in members {
265             let member_obj = member.as_object().unwrap();
266             let jtargets = member_obj.get("targets").unwrap().as_array().unwrap();
267             for jtarget in jtargets {
268                 targets.push(target_from_json(jtarget));
269             }
270         }
271         return Ok(targets);
272     }
273     Err(std::io::Error::new(
274         std::io::ErrorKind::NotFound,
275         str::from_utf8(&output.stderr).unwrap(),
276     ))
277
278 }
279
280 fn target_from_json(jtarget: &Value) -> Target {
281     let jtarget = jtarget.as_object().unwrap();
282     let path = PathBuf::from(jtarget.get("src_path").unwrap().as_str().unwrap());
283     let kinds = jtarget.get("kind").unwrap().as_array().unwrap();
284     let kind = match kinds[0].as_str().unwrap() {
285         "bin" => TargetKind::Bin,
286         "lib" | "dylib" | "staticlib" | "cdylib" | "rlib" => TargetKind::Lib,
287         "test" => TargetKind::Test,
288         "example" => TargetKind::Example,
289         "bench" => TargetKind::Bench,
290         "custom-build" => TargetKind::CustomBuild,
291         "proc-macro" => TargetKind::ProcMacro,
292         _ => TargetKind::Other,
293     };
294
295     Target {
296         path: path,
297         kind: kind,
298     }
299 }
300
301 fn format_files(
302     files: &[PathBuf],
303     fmt_args: &[String],
304     verbosity: Verbosity,
305 ) -> Result<ExitStatus, std::io::Error> {
306     let stdout = if verbosity == Verbosity::Quiet {
307         std::process::Stdio::null()
308     } else {
309         std::process::Stdio::inherit()
310     };
311     if verbosity == Verbosity::Verbose {
312         print!("rustfmt");
313         for a in fmt_args.iter() {
314             print!(" {}", a);
315         }
316         for f in files.iter() {
317             print!(" {}", f.display());
318         }
319         println!("");
320     }
321     let mut command = Command::new("rustfmt")
322         .stdout(stdout)
323         .args(files)
324         .args(fmt_args)
325         .spawn()
326         .map_err(|e| match e.kind() {
327             std::io::ErrorKind::NotFound => {
328                 std::io::Error::new(
329                     std::io::ErrorKind::Other,
330                     "Could not run rustfmt, please make sure it is in your PATH.",
331                 )
332             }
333             _ => e,
334         })?;
335     command.wait()
336 }