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