]> git.lizzy.rs Git - rust.git/blob - src/tools/rustfmt/src/bin/main.rs
Auto merge of #87430 - devnexen:netbsd_ucred_enabled, r=joshtriplett
[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]");
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     opts.optflagopt(
182         "h",
183         "help",
184         "Show this message or help about a specific topic: `config` or `file-lines`",
185         "=TOPIC",
186     );
187
188     opts
189 }
190
191 fn is_nightly() -> bool {
192     option_env!("CFG_RELEASE_CHANNEL").map_or(true, |c| c == "nightly" || c == "dev")
193 }
194
195 // Returned i32 is an exit code
196 fn execute(opts: &Options) -> Result<i32> {
197     let matches = opts.parse(env::args().skip(1))?;
198     let options = GetOptsOptions::from_matches(&matches)?;
199
200     match determine_operation(&matches)? {
201         Operation::Help(HelpOp::None) => {
202             print_usage_to_stdout(opts, "");
203             Ok(0)
204         }
205         Operation::Help(HelpOp::Config) => {
206             Config::print_docs(&mut stdout(), options.unstable_features);
207             Ok(0)
208         }
209         Operation::Help(HelpOp::FileLines) => {
210             print_help_file_lines();
211             Ok(0)
212         }
213         Operation::Version => {
214             print_version();
215             Ok(0)
216         }
217         Operation::ConfigOutputDefault { path } => {
218             let toml = Config::default().all_options().to_toml()?;
219             if let Some(path) = path {
220                 let mut file = File::create(path)?;
221                 file.write_all(toml.as_bytes())?;
222             } else {
223                 io::stdout().write_all(toml.as_bytes())?;
224             }
225             Ok(0)
226         }
227         Operation::ConfigOutputCurrent { path } => {
228             let path = match path {
229                 Some(path) => path,
230                 None => return Err(format_err!("PATH required for `--print-config current`")),
231             };
232
233             let file = PathBuf::from(path);
234             let file = file.canonicalize().unwrap_or(file);
235
236             let (config, _) = load_config(Some(file.parent().unwrap()), Some(options))?;
237             let toml = config.all_options().to_toml()?;
238             io::stdout().write_all(toml.as_bytes())?;
239
240             Ok(0)
241         }
242         Operation::Stdin { input } => format_string(input, options),
243         Operation::Format {
244             files,
245             minimal_config_path,
246         } => format(files, minimal_config_path, &options),
247     }
248 }
249
250 fn format_string(input: String, options: GetOptsOptions) -> Result<i32> {
251     // try to read config from local directory
252     let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
253
254     if options.check {
255         return Err(OperationError::CheckWithStdin.into());
256     }
257     if let Some(emit_mode) = options.emit_mode {
258         if emit_mode != EmitMode::Stdout {
259             return Err(OperationError::EmitWithStdin.into());
260         }
261     }
262     // emit mode is always Stdout for Stdin.
263     config.set().emit_mode(EmitMode::Stdout);
264     config.set().verbose(Verbosity::Quiet);
265
266     // parse file_lines
267     config.set().file_lines(options.file_lines);
268     for f in config.file_lines().files() {
269         match *f {
270             FileName::Stdin => {}
271             _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f),
272         }
273     }
274
275     let out = &mut stdout();
276     let mut session = Session::new(config, Some(out));
277     format_and_emit_report(&mut session, Input::Text(input));
278
279     let exit_code = if session.has_operational_errors() || session.has_parsing_errors() {
280         1
281     } else {
282         0
283     };
284     Ok(exit_code)
285 }
286
287 fn format(
288     files: Vec<PathBuf>,
289     minimal_config_path: Option<String>,
290     options: &GetOptsOptions,
291 ) -> Result<i32> {
292     options.verify_file_lines(&files);
293     let (config, config_path) = load_config(None, Some(options.clone()))?;
294
295     if config.verbose() == Verbosity::Verbose {
296         if let Some(path) = config_path.as_ref() {
297             println!("Using rustfmt config file {}", path.display());
298         }
299     }
300
301     let out = &mut stdout();
302     let mut session = Session::new(config, Some(out));
303
304     for file in files {
305         if !file.exists() {
306             eprintln!("Error: file `{}` does not exist", file.to_str().unwrap());
307             session.add_operational_error();
308         } else if file.is_dir() {
309             eprintln!("Error: `{}` is a directory", file.to_str().unwrap());
310             session.add_operational_error();
311         } else {
312             // Check the file directory if the config-path could not be read or not provided
313             if config_path.is_none() {
314                 let (local_config, config_path) =
315                     load_config(Some(file.parent().unwrap()), Some(options.clone()))?;
316                 if local_config.verbose() == Verbosity::Verbose {
317                     if let Some(path) = config_path {
318                         println!(
319                             "Using rustfmt config file {} for {}",
320                             path.display(),
321                             file.display()
322                         );
323                     }
324                 }
325
326                 session.override_config(local_config, |sess| {
327                     format_and_emit_report(sess, Input::File(file))
328                 });
329             } else {
330                 format_and_emit_report(&mut session, Input::File(file));
331             }
332         }
333     }
334
335     // If we were given a path via dump-minimal-config, output any options
336     // that were used during formatting as TOML.
337     if let Some(path) = minimal_config_path {
338         let mut file = File::create(path)?;
339         let toml = session.config.used_options().to_toml()?;
340         file.write_all(toml.as_bytes())?;
341     }
342
343     let exit_code = if session.has_operational_errors()
344         || session.has_parsing_errors()
345         || ((session.has_diff() || session.has_check_errors()) && options.check)
346     {
347         1
348     } else {
349         0
350     };
351     Ok(exit_code)
352 }
353
354 fn format_and_emit_report<T: Write>(session: &mut Session<'_, T>, input: Input) {
355     match session.format(input) {
356         Ok(report) => {
357             if report.has_warnings() {
358                 eprintln!(
359                     "{}",
360                     FormatReportFormatterBuilder::new(&report)
361                         .enable_colors(should_print_with_colors(session))
362                         .build()
363                 );
364             }
365         }
366         Err(msg) => {
367             eprintln!("Error writing files: {}", msg);
368             session.add_operational_error();
369         }
370     }
371 }
372
373 fn should_print_with_colors<T: Write>(session: &mut Session<'_, T>) -> bool {
374     match term::stderr() {
375         Some(ref t)
376             if session.config.color().use_colored_tty()
377                 && t.supports_color()
378                 && t.supports_attr(term::Attr::Bold) =>
379         {
380             true
381         }
382         _ => false,
383     }
384 }
385
386 fn print_usage_to_stdout(opts: &Options, reason: &str) {
387     let sep = if reason.is_empty() {
388         String::new()
389     } else {
390         format!("{}\n\n", reason)
391     };
392     let msg = format!(
393         "{}Format Rust code\n\nusage: {} [options] <file>...",
394         sep,
395         env::args_os().next().unwrap().to_string_lossy()
396     );
397     println!("{}", opts.usage(&msg));
398 }
399
400 fn print_help_file_lines() {
401     println!(
402         "If you want to restrict reformatting to specific sets of lines, you can
403 use the `--file-lines` option. Its argument is a JSON array of objects
404 with `file` and `range` properties, where `file` is a file name, and
405 `range` is an array representing a range of lines like `[7,13]`. Ranges
406 are 1-based and inclusive of both end points. Specifying an empty array
407 will result in no files being formatted. For example,
408
409 ```
410 rustfmt --file-lines '[
411     {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
412     {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
413     {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
414     {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
415 ```
416
417 would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
418 and `15` of `src/foo.rs`. No other files would be formatted, even if they
419 are included as out of line modules from `src/lib.rs`."
420     );
421 }
422
423 fn print_version() {
424     let version_info = format!(
425         "{}-{}",
426         option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
427         include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
428     );
429
430     println!("rustfmt {}", version_info);
431 }
432
433 fn determine_operation(matches: &Matches) -> Result<Operation, OperationError> {
434     if matches.opt_present("h") {
435         let topic = matches.opt_str("h");
436         if topic == None {
437             return Ok(Operation::Help(HelpOp::None));
438         } else if topic == Some("config".to_owned()) {
439             return Ok(Operation::Help(HelpOp::Config));
440         } else if topic == Some("file-lines".to_owned()) {
441             return Ok(Operation::Help(HelpOp::FileLines));
442         } else {
443             return Err(OperationError::UnknownHelpTopic(topic.unwrap()));
444         }
445     }
446     let mut free_matches = matches.free.iter();
447
448     let mut minimal_config_path = None;
449     if let Some(kind) = matches.opt_str("print-config") {
450         let path = free_matches.next().cloned();
451         match kind.as_str() {
452             "default" => return Ok(Operation::ConfigOutputDefault { path }),
453             "current" => return Ok(Operation::ConfigOutputCurrent { path }),
454             "minimal" => {
455                 minimal_config_path = path;
456                 if minimal_config_path.is_none() {
457                     eprintln!("WARNING: PATH required for `--print-config minimal`.");
458                 }
459             }
460             _ => {
461                 return Err(OperationError::UnknownPrintConfigTopic(kind));
462             }
463         }
464     }
465
466     if matches.opt_present("version") {
467         return Ok(Operation::Version);
468     }
469
470     let files: Vec<_> = free_matches
471         .map(|s| {
472             let p = PathBuf::from(s);
473             // we will do comparison later, so here tries to canonicalize first
474             // to get the expected behavior.
475             p.canonicalize().unwrap_or(p)
476         })
477         .collect();
478
479     // if no file argument is supplied, read from stdin
480     if files.is_empty() {
481         if minimal_config_path.is_some() {
482             return Err(OperationError::MinimalPathWithStdin);
483         }
484         let mut buffer = String::new();
485         io::stdin().read_to_string(&mut buffer)?;
486
487         return Ok(Operation::Stdin { input: buffer });
488     }
489
490     Ok(Operation::Format {
491         files,
492         minimal_config_path,
493     })
494 }
495
496 const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
497
498 /// Parsed command line options.
499 #[derive(Clone, Debug, Default)]
500 struct GetOptsOptions {
501     skip_children: Option<bool>,
502     quiet: bool,
503     verbose: bool,
504     config_path: Option<PathBuf>,
505     inline_config: HashMap<String, String>,
506     emit_mode: Option<EmitMode>,
507     backup: bool,
508     check: bool,
509     edition: Option<Edition>,
510     color: Option<Color>,
511     file_lines: FileLines, // Default is all lines in all files.
512     unstable_features: bool,
513     error_on_unformatted: Option<bool>,
514     print_misformatted_file_names: bool,
515 }
516
517 impl GetOptsOptions {
518     pub fn from_matches(matches: &Matches) -> Result<GetOptsOptions> {
519         let mut options = GetOptsOptions::default();
520         options.verbose = matches.opt_present("verbose");
521         options.quiet = matches.opt_present("quiet");
522         if options.verbose && options.quiet {
523             return Err(format_err!("Can't use both `--verbose` and `--quiet`"));
524         }
525
526         let rust_nightly = is_nightly();
527
528         if rust_nightly {
529             options.unstable_features = matches.opt_present("unstable-features");
530
531             if options.unstable_features {
532                 if matches.opt_present("skip-children") {
533                     options.skip_children = Some(true);
534                 }
535                 if matches.opt_present("error-on-unformatted") {
536                     options.error_on_unformatted = Some(true);
537                 }
538                 if let Some(ref file_lines) = matches.opt_str("file-lines") {
539                     options.file_lines = file_lines.parse()?;
540                 }
541             } else {
542                 let mut unstable_options = vec![];
543                 if matches.opt_present("skip-children") {
544                     unstable_options.push("`--skip-children`");
545                 }
546                 if matches.opt_present("error-on-unformatted") {
547                     unstable_options.push("`--error-on-unformatted`");
548                 }
549                 if matches.opt_present("file-lines") {
550                     unstable_options.push("`--file-lines`");
551                 }
552                 if !unstable_options.is_empty() {
553                     let s = if unstable_options.len() == 1 { "" } else { "s" };
554                     return Err(format_err!(
555                         "Unstable option{} ({}) used without `--unstable-features`",
556                         s,
557                         unstable_options.join(", "),
558                     ));
559                 }
560             }
561         }
562
563         options.config_path = matches.opt_str("config-path").map(PathBuf::from);
564
565         options.inline_config = matches
566             .opt_strs("config")
567             .iter()
568             .flat_map(|config| config.split(','))
569             .map(
570                 |key_val| match key_val.char_indices().find(|(_, ch)| *ch == '=') {
571                     Some((middle, _)) => {
572                         let (key, val) = (&key_val[..middle], &key_val[middle + 1..]);
573                         if !Config::is_valid_key_val(key, val) {
574                             Err(format_err!("invalid key=val pair: `{}`", key_val))
575                         } else {
576                             Ok((key.to_string(), val.to_string()))
577                         }
578                     }
579
580                     None => Err(format_err!(
581                         "--config expects comma-separated list of key=val pairs, found `{}`",
582                         key_val
583                     )),
584                 },
585             )
586             .collect::<Result<HashMap<_, _>, _>>()?;
587
588         options.check = matches.opt_present("check");
589         if let Some(ref emit_str) = matches.opt_str("emit") {
590             if options.check {
591                 return Err(format_err!("Invalid to use `--emit` and `--check`"));
592             }
593
594             options.emit_mode = Some(emit_mode_from_emit_str(emit_str)?);
595         }
596
597         if let Some(ref edition_str) = matches.opt_str("edition") {
598             options.edition = Some(edition_from_edition_str(edition_str)?);
599         }
600
601         if matches.opt_present("backup") {
602             options.backup = true;
603         }
604
605         if matches.opt_present("files-with-diff") {
606             options.print_misformatted_file_names = true;
607         }
608
609         if !rust_nightly {
610             if let Some(ref emit_mode) = options.emit_mode {
611                 if !STABLE_EMIT_MODES.contains(emit_mode) {
612                     return Err(format_err!(
613                         "Invalid value for `--emit` - using an unstable \
614                          value without `--unstable-features`",
615                     ));
616                 }
617             }
618         }
619
620         if let Some(ref color) = matches.opt_str("color") {
621             match Color::from_str(color) {
622                 Ok(color) => options.color = Some(color),
623                 _ => return Err(format_err!("Invalid color: {}", color)),
624             }
625         }
626
627         Ok(options)
628     }
629
630     fn verify_file_lines(&self, files: &[PathBuf]) {
631         for f in self.file_lines.files() {
632             match *f {
633                 FileName::Real(ref f) if files.contains(f) => {}
634                 FileName::Real(_) => {
635                     eprintln!("Warning: Extra file listed in file_lines option '{}'", f)
636                 }
637                 FileName::Stdin => eprintln!("Warning: Not a file '{}'", f),
638             }
639         }
640     }
641 }
642
643 impl CliOptions for GetOptsOptions {
644     fn apply_to(self, config: &mut Config) {
645         if self.verbose {
646             config.set().verbose(Verbosity::Verbose);
647         } else if self.quiet {
648             config.set().verbose(Verbosity::Quiet);
649         } else {
650             config.set().verbose(Verbosity::Normal);
651         }
652         config.set().file_lines(self.file_lines);
653         config.set().unstable_features(self.unstable_features);
654         if let Some(skip_children) = self.skip_children {
655             config.set().skip_children(skip_children);
656         }
657         if let Some(error_on_unformatted) = self.error_on_unformatted {
658             config.set().error_on_unformatted(error_on_unformatted);
659         }
660         if let Some(edition) = self.edition {
661             config.set().edition(edition);
662         }
663         if self.check {
664             config.set().emit_mode(EmitMode::Diff);
665         } else if let Some(emit_mode) = self.emit_mode {
666             config.set().emit_mode(emit_mode);
667         }
668         if self.backup {
669             config.set().make_backup(true);
670         }
671         if let Some(color) = self.color {
672             config.set().color(color);
673         }
674         if self.print_misformatted_file_names {
675             config.set().print_misformatted_file_names(true);
676         }
677
678         for (key, val) in self.inline_config {
679             config.override_value(&key, &val);
680         }
681     }
682
683     fn config_path(&self) -> Option<&Path> {
684         self.config_path.as_deref()
685     }
686 }
687
688 fn edition_from_edition_str(edition_str: &str) -> Result<Edition> {
689     match edition_str {
690         "2015" => Ok(Edition::Edition2015),
691         "2018" => Ok(Edition::Edition2018),
692         _ => Err(format_err!("Invalid value for `--edition`")),
693     }
694 }
695
696 fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode> {
697     match emit_str {
698         "files" => Ok(EmitMode::Files),
699         "stdout" => Ok(EmitMode::Stdout),
700         "coverage" => Ok(EmitMode::Coverage),
701         "checkstyle" => Ok(EmitMode::Checkstyle),
702         "json" => Ok(EmitMode::Json),
703         _ => Err(format_err!("Invalid value for `--emit`")),
704     }
705 }