]> git.lizzy.rs Git - rust.git/blob - src/bin/main.rs
set of clippy changes
[rust.git] / src / bin / main.rs
1 // Copyright 2015 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 #![cfg(not(test))]
12 #![feature(extern_prelude)]
13
14 extern crate env_logger;
15 #[macro_use]
16 extern crate failure;
17 extern crate getopts;
18 extern crate rustfmt_nightly as rustfmt;
19
20 use std::env;
21 use std::fs::File;
22 use std::io::{self, stdout, Read, Write};
23 use std::path::{Path, PathBuf};
24 use std::str::FromStr;
25
26 use failure::err_msg;
27
28 use getopts::{Matches, Options};
29
30 use rustfmt::{
31     load_config, CliOptions, Color, Config, EmitMode, ErrorKind, FileLines, FileName, Input,
32     Session, Verbosity,
33 };
34
35 fn main() {
36     env_logger::init();
37     let opts = make_opts();
38
39     let exit_code = match execute(&opts) {
40         Ok(code) => code,
41         Err(e) => {
42             eprintln!("{}", e.to_string());
43             1
44         }
45     };
46     // Make sure standard output is flushed before we exit.
47     std::io::stdout().flush().unwrap();
48
49     // Exit with given exit code.
50     //
51     // NOTE: This immediately terminates the process without doing any cleanup,
52     // so make sure to finish all necessary cleanup before this is called.
53     std::process::exit(exit_code);
54 }
55
56 /// Rustfmt operations.
57 enum Operation {
58     /// Format files and their child modules.
59     Format {
60         files: Vec<PathBuf>,
61         minimal_config_path: Option<String>,
62     },
63     /// Print the help message.
64     Help(HelpOp),
65     // Print version information
66     Version,
67     /// Output default config to a file, or stdout if None
68     ConfigOutputDefault {
69         path: Option<String>,
70     },
71     /// No file specified, read from stdin
72     Stdin {
73         input: String,
74     },
75 }
76
77 /// Arguments to `--help`
78 enum HelpOp {
79     None,
80     Config,
81     FileLines,
82 }
83
84 fn make_opts() -> Options {
85     let mut opts = Options::new();
86
87     opts.optflag(
88         "",
89         "check",
90         "Run in 'check' mode. Exits with 0 if input if formatted correctly. Exits \
91          with 1 and prints a diff if formatting is required.",
92     );
93     let is_nightly = is_nightly();
94     let emit_opts = if is_nightly {
95         "[files|stdout|coverage|checkstyle]"
96     } else {
97         "[files|stdout]"
98     };
99     opts.optopt("", "emit", "What data to emit and how", emit_opts);
100     opts.optflag("", "backup", "Backup any modified files.");
101     opts.optopt(
102         "",
103         "config-path",
104         "Recursively searches the given path for the rustfmt.toml config file. If not \
105          found reverts to the input file path",
106         "[Path for the configuration file]",
107     );
108     opts.optopt(
109         "",
110         "color",
111         "Use colored output (if supported)",
112         "[always|never|auto]",
113     );
114     opts.optopt(
115         "",
116         "print-config",
117         "Dumps a default or minimal config to PATH. A minimal config is the \
118          subset of the current config file used for formatting the current program.",
119         "[minimal|default] PATH",
120     );
121
122     if is_nightly {
123         opts.optflag(
124             "",
125             "unstable-features",
126             "Enables unstable features. Only available on nightly channel.",
127         );
128         opts.optopt(
129             "",
130             "file-lines",
131             "Format specified line ranges. Run with `--help=file-lines` for \
132              more detail (unstable).",
133             "JSON",
134         );
135         opts.optflag(
136             "",
137             "error-on-unformatted",
138             "Error if unable to get comments or string literals within max_width, \
139              or they are left with trailing whitespaces (unstable).",
140         );
141         opts.optflag(
142             "",
143             "skip-children",
144             "Don't reformat child modules (unstable).",
145         );
146     }
147
148     opts.optflag("v", "verbose", "Print verbose output");
149     opts.optflag("q", "quiet", "Print less output");
150     opts.optflag("V", "version", "Show version information");
151     opts.optflagopt(
152         "h",
153         "help",
154         "Show this message or help about a specific topic: `config` or `file-lines`",
155         "=TOPIC",
156     );
157
158     opts
159 }
160
161 fn is_nightly() -> bool {
162     option_env!("CFG_RELEASE_CHANNEL")
163         .map(|c| c == "nightly" || c == "dev")
164         .unwrap_or(false)
165 }
166
167 // Returned i32 is an exit code
168 fn execute(opts: &Options) -> Result<i32, failure::Error> {
169     let matches = opts.parse(env::args().skip(1))?;
170     let options = GetOptsOptions::from_matches(&matches)?;
171
172     match determine_operation(&matches)? {
173         Operation::Help(HelpOp::None) => {
174             print_usage_to_stdout(opts, "");
175             return Ok(0);
176         }
177         Operation::Help(HelpOp::Config) => {
178             Config::print_docs(&mut stdout(), options.unstable_features);
179             return Ok(0);
180         }
181         Operation::Help(HelpOp::FileLines) => {
182             print_help_file_lines();
183             return Ok(0);
184         }
185         Operation::Version => {
186             print_version();
187             return Ok(0);
188         }
189         Operation::ConfigOutputDefault { path } => {
190             let toml = Config::default().all_options().to_toml().map_err(err_msg)?;
191             if let Some(path) = path {
192                 let mut file = File::create(path)?;
193                 file.write_all(toml.as_bytes())?;
194             } else {
195                 io::stdout().write_all(toml.as_bytes())?;
196             }
197             return Ok(0);
198         }
199         Operation::Stdin { input } => format_string(input, options),
200         Operation::Format {
201             files,
202             minimal_config_path,
203         } => format(files, minimal_config_path, options),
204     }
205 }
206
207 fn format_string(input: String, options: GetOptsOptions) -> Result<i32, failure::Error> {
208     // try to read config from local directory
209     let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
210
211     // emit mode is always Stdout for Stdin.
212     config.set().emit_mode(EmitMode::Stdout);
213     config.set().verbose(Verbosity::Quiet);
214
215     // parse file_lines
216     config.set().file_lines(options.file_lines);
217     for f in config.file_lines().files() {
218         match *f {
219             FileName::Stdin => {}
220             _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
221         }
222     }
223
224     let out = &mut stdout();
225     let mut session = Session::new(config, Some(out));
226     format_and_emit_report(&mut session, Input::Text(input));
227
228     let exit_code = if session.has_operational_errors() || session.has_parsing_errors() {
229         1
230     } else {
231         0
232     };
233     Ok(exit_code)
234 }
235
236 fn format(
237     files: Vec<PathBuf>,
238     minimal_config_path: Option<String>,
239     options: GetOptsOptions,
240 ) -> Result<i32, failure::Error> {
241     options.verify_file_lines(&files);
242     let (config, config_path) = load_config(None, Some(options.clone()))?;
243
244     if config.verbose() == Verbosity::Verbose {
245         if let Some(path) = config_path.as_ref() {
246             println!("Using rustfmt config file {}", path.display());
247         }
248     }
249
250     let out = &mut stdout();
251     let mut session = Session::new(config, Some(out));
252
253     for file in files {
254         if !file.exists() {
255             eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
256             session.add_operational_error();
257         } else if file.is_dir() {
258             eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
259             session.add_operational_error();
260         } else {
261             // Check the file directory if the config-path could not be read or not provided
262             if config_path.is_none() {
263                 let (local_config, config_path) =
264                     load_config(Some(file.parent().unwrap()), Some(options.clone()))?;
265                 if local_config.verbose() == Verbosity::Verbose {
266                     if let Some(path) = config_path {
267                         println!(
268                             "Using rustfmt config file {} for {}",
269                             path.display(),
270                             file.display()
271                         );
272                     }
273                 }
274
275                 session.override_config(local_config, |sess| {
276                     format_and_emit_report(sess, Input::File(file))
277                 });
278             } else {
279                 format_and_emit_report(&mut session, Input::File(file));
280             }
281         }
282     }
283
284     // If we were given a path via dump-minimal-config, output any options
285     // that were used during formatting as TOML.
286     if let Some(path) = minimal_config_path {
287         let mut file = File::create(path)?;
288         let toml = session.config.used_options().to_toml().map_err(err_msg)?;
289         file.write_all(toml.as_bytes())?;
290     }
291
292     let exit_code = if session.has_operational_errors()
293         || session.has_parsing_errors()
294         || ((session.has_diff() || session.has_check_errors()) && options.check)
295     {
296         1
297     } else {
298         0
299     };
300     Ok(exit_code)
301 }
302
303 fn format_and_emit_report<T: Write>(session: &mut Session<T>, input: Input) {
304     match session.format(input) {
305         Ok(report) => {
306             if report.has_warnings() {
307                 match term::stderr() {
308                     Some(ref t)
309                         if session.config.color().use_colored_tty()
310                             && t.supports_color()
311                             && t.supports_attr(term::Attr::Bold) =>
312                     {
313                         match report.fancy_print(term::stderr().unwrap()) {
314                             Ok(..) => (),
315                             Err(..) => panic!("Unable to write to stderr: {}", report),
316                         }
317                     }
318                     _ => eprintln!("{}", report),
319                 }
320             }
321         }
322         Err(msg) => {
323             eprintln!("Error writing files: {}", msg);
324             session.add_operational_error();
325         }
326     }
327 }
328
329 fn print_usage_to_stdout(opts: &Options, reason: &str) {
330     let sep = if reason.is_empty() {
331         String::new()
332     } else {
333         format!("{}\n\n", reason)
334     };
335     let msg = format!(
336         "{}Format Rust code\n\nusage: {} [options] <file>...",
337         sep,
338         env::args_os().next().unwrap().to_string_lossy()
339     );
340     println!("{}", opts.usage(&msg));
341 }
342
343 fn print_help_file_lines() {
344     println!(
345         "If you want to restrict reformatting to specific sets of lines, you can
346 use the `--file-lines` option. Its argument is a JSON array of objects
347 with `file` and `range` properties, where `file` is a file name, and
348 `range` is an array representing a range of lines like `[7,13]`. Ranges
349 are 1-based and inclusive of both end points. Specifying an empty array
350 will result in no files being formatted. For example,
351
352 ```
353 rustfmt --file-lines '[
354     {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
355     {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
356     {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
357     {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
358 ```
359
360 would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
361 and `15` of `src/foo.rs`. No other files would be formatted, even if they
362 are included as out of line modules from `src/lib.rs`."
363     );
364 }
365
366 fn print_version() {
367     let version_info = format!(
368         "{}-{}",
369         option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
370         include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
371     );
372
373     println!("rustfmt {}", version_info);
374 }
375
376 fn determine_operation(matches: &Matches) -> Result<Operation, ErrorKind> {
377     if matches.opt_present("h") {
378         let topic = matches.opt_str("h");
379         if topic == None {
380             return Ok(Operation::Help(HelpOp::None));
381         } else if topic == Some("config".to_owned()) {
382             return Ok(Operation::Help(HelpOp::Config));
383         } else if topic == Some("file-lines".to_owned()) {
384             return Ok(Operation::Help(HelpOp::FileLines));
385         } else {
386             println!("Unknown help topic: `{}`\n", topic.unwrap());
387             return Ok(Operation::Help(HelpOp::None));
388         }
389     }
390
391     let mut minimal_config_path = None;
392     if let Some(ref kind) = matches.opt_str("print-config") {
393         let path = matches.free.get(0).cloned();
394         if kind == "default" {
395             return Ok(Operation::ConfigOutputDefault { path });
396         } else if kind == "minimal" {
397             minimal_config_path = path;
398             if minimal_config_path.is_none() {
399                 println!("WARNING: PATH required for `--print-config minimal`");
400             }
401         }
402     }
403
404     if matches.opt_present("version") {
405         return Ok(Operation::Version);
406     }
407
408     // if no file argument is supplied, read from stdin
409     if matches.free.is_empty() {
410         let mut buffer = String::new();
411         io::stdin().read_to_string(&mut buffer)?;
412
413         return Ok(Operation::Stdin { input: buffer });
414     }
415
416     let files: Vec<_> = matches
417         .free
418         .iter()
419         .map(|s| {
420             let p = PathBuf::from(s);
421             // we will do comparison later, so here tries to canonicalize first
422             // to get the expected behavior.
423             p.canonicalize().unwrap_or(p)
424         }).collect();
425
426     Ok(Operation::Format {
427         files,
428         minimal_config_path,
429     })
430 }
431
432 const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
433
434 /// Parsed command line options.
435 #[derive(Clone, Debug, Default)]
436 struct GetOptsOptions {
437     skip_children: Option<bool>,
438     quiet: bool,
439     verbose: bool,
440     config_path: Option<PathBuf>,
441     emit_mode: EmitMode,
442     backup: bool,
443     check: bool,
444     color: Option<Color>,
445     file_lines: FileLines, // Default is all lines in all files.
446     unstable_features: bool,
447     error_on_unformatted: Option<bool>,
448 }
449
450 impl GetOptsOptions {
451     pub fn from_matches(matches: &Matches) -> Result<GetOptsOptions, failure::Error> {
452         let mut options = GetOptsOptions::default();
453         options.verbose = matches.opt_present("verbose");
454         options.quiet = matches.opt_present("quiet");
455         if options.verbose && options.quiet {
456             return Err(format_err!("Can't use both `--verbose` and `--quiet`"));
457         }
458
459         let rust_nightly = is_nightly();
460
461         if rust_nightly {
462             options.unstable_features = matches.opt_present("unstable-features");
463
464             if options.unstable_features {
465                 if matches.opt_present("skip-children") {
466                     options.skip_children = Some(true);
467                 }
468                 if matches.opt_present("error-on-unformatted") {
469                     options.error_on_unformatted = Some(true);
470                 }
471                 if let Some(ref file_lines) = matches.opt_str("file-lines") {
472                     options.file_lines = file_lines.parse().map_err(err_msg)?;
473                 }
474             } else {
475                 let mut unstable_options = vec![];
476                 if matches.opt_present("skip-children") {
477                     unstable_options.push("`--skip-children`");
478                 }
479                 if matches.opt_present("error-on-unformatted") {
480                     unstable_options.push("`--error-on-unformatted`");
481                 }
482                 if matches.opt_present("file-lines") {
483                     unstable_options.push("`--file-lines`");
484                 }
485                 if !unstable_options.is_empty() {
486                     let s = if unstable_options.len() == 1 { "" } else { "s" };
487                     return Err(format_err!(
488                         "Unstable option{} ({}) used without `--unstable-features`",
489                         s,
490                         unstable_options.join(", "),
491                     ));
492                 }
493             }
494         }
495
496         options.config_path = matches.opt_str("config-path").map(PathBuf::from);
497
498         options.check = matches.opt_present("check");
499         if let Some(ref emit_str) = matches.opt_str("emit") {
500             if options.check {
501                 return Err(format_err!("Invalid to use `--emit` and `--check`"));
502             }
503             if let Ok(emit_mode) = emit_mode_from_emit_str(emit_str) {
504                 options.emit_mode = emit_mode;
505             } else {
506                 return Err(format_err!("Invalid value for `--emit`"));
507             }
508         }
509
510         if matches.opt_present("backup") {
511             options.backup = true;
512         }
513
514         if !rust_nightly {
515             if !STABLE_EMIT_MODES.contains(&options.emit_mode) {
516                 return Err(format_err!(
517                     "Invalid value for `--emit` - using an unstable \
518                      value without `--unstable-features`",
519                 ));
520             }
521         }
522
523         if let Some(ref color) = matches.opt_str("color") {
524             match Color::from_str(color) {
525                 Ok(color) => options.color = Some(color),
526                 _ => return Err(format_err!("Invalid color: {}", color)),
527             }
528         }
529
530         Ok(options)
531     }
532
533     fn verify_file_lines(&self, files: &[PathBuf]) {
534         for f in self.file_lines.files() {
535             match *f {
536                 FileName::Real(ref f) if files.contains(f) => {}
537                 FileName::Real(_) => {
538                     eprintln!("Warning: Extra file listed in file_lines option '{}'", f)
539                 }
540                 FileName::Stdin => eprintln!("Warning: Not a file '{}'", f),
541             }
542         }
543     }
544 }
545
546 impl CliOptions for GetOptsOptions {
547     fn apply_to(self, config: &mut Config) {
548         if self.verbose {
549             config.set().verbose(Verbosity::Verbose);
550         } else if self.quiet {
551             config.set().verbose(Verbosity::Quiet);
552         } else {
553             config.set().verbose(Verbosity::Normal);
554         }
555         config.set().file_lines(self.file_lines);
556         config.set().unstable_features(self.unstable_features);
557         if let Some(skip_children) = self.skip_children {
558             config.set().skip_children(skip_children);
559         }
560         if let Some(error_on_unformatted) = self.error_on_unformatted {
561             config.set().error_on_unformatted(error_on_unformatted);
562         }
563         if self.check {
564             config.set().emit_mode(EmitMode::Diff);
565         } else {
566             config.set().emit_mode(self.emit_mode);
567         }
568         if self.backup {
569             config.set().make_backup(true);
570         }
571         if let Some(color) = self.color {
572             config.set().color(color);
573         }
574     }
575
576     fn config_path(&self) -> Option<&Path> {
577         self.config_path.as_ref().map(|p| &**p)
578     }
579 }
580
581 fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode, failure::Error> {
582     match emit_str {
583         "files" => Ok(EmitMode::Files),
584         "stdout" => Ok(EmitMode::Stdout),
585         "coverage" => Ok(EmitMode::Coverage),
586         "checkstyle" => Ok(EmitMode::Checkstyle),
587         _ => Err(format_err!("Invalid value for `--emit`")),
588     }
589 }