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