]> git.proxmox.com Git - rustc.git/blame - src/tools/rustfmt/src/bin/main.rs
New upstream version 1.53.0+dfsg1
[rustc.git] / src / tools / rustfmt / src / bin / main.rs
CommitLineData
f20569fa 1use anyhow::{format_err, Result};
cdc7bbd5 2
f20569fa
XL
3use io::Error as IoError;
4use thiserror::Error;
5
6use rustfmt_nightly as rustfmt;
7
8use std::collections::HashMap;
9use std::env;
10use std::fs::File;
11use std::io::{self, stdout, Read, Write};
12use std::path::{Path, PathBuf};
13use std::str::FromStr;
14
15use getopts::{Matches, Options};
16
17use crate::rustfmt::{
18 load_config, CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName,
19 FormatReportFormatterBuilder, Input, Session, Verbosity,
20};
21
22fn 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.
44enum 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)]
64pub 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
87impl From<IoError> for OperationError {
88 fn from(e: IoError) -> OperationError {
89 OperationError::IoError(e)
90 }
91}
92
93/// Arguments to `--help`
94enum HelpOp {
95 None,
96 Config,
97 FileLines,
98}
99
100fn 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
191fn 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
196fn 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
cdc7bbd5 236 let (config, _) = load_config(Some(file.parent().unwrap()), Some(options))?;
f20569fa
XL
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
250fn 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
287fn 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
354fn 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
373fn 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
386fn 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
400fn print_help_file_lines() {
401 println!(
402 "If you want to restrict reformatting to specific sets of lines, you can
403use the `--file-lines` option. Its argument is a JSON array of objects
404with `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
406are 1-based and inclusive of both end points. Specifying an empty array
407will result in no files being formatted. For example,
408
409```
410rustfmt --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
417would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
418and `15` of `src/foo.rs`. No other files would be formatted, even if they
419are included as out of line modules from `src/lib.rs`."
420 );
421}
422
423fn 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
433fn 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
496const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
497
498/// Parsed command line options.
499#[derive(Clone, Debug, Default)]
500struct 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
517impl 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()
cdc7bbd5 568 .flat_map(|config| config.split(','))
f20569fa
XL
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
643impl 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> {
cdc7bbd5 684 self.config_path.as_deref()
f20569fa
XL
685 }
686}
687
688fn 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
696fn 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}