Skip to main content

rustfmt/
main.rs

1#![feature(rustc_private)]
2
3use anyhow::{Result, format_err};
4
5use io::Error as IoError;
6use thiserror::Error;
7
8use rustfmt_nightly as rustfmt;
9use tracing_subscriber::EnvFilter;
10
11use std::collections::HashMap;
12use std::env;
13use std::fs::File;
14use std::io::{self, Read, Write, stdout};
15use std::path::{Path, PathBuf};
16use std::str::FromStr;
17
18use getopts::{Matches, Options};
19
20use crate::rustfmt::{
21    CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName,
22    FormatReportFormatterBuilder, Input, Session, StyleEdition, Verbosity, Version, load_config,
23};
24
25const BUG_REPORT_URL: &str = "https://github.com/rust-lang/rustfmt/issues/new?labels=bug";
26
27// N.B. these crates are loaded from the sysroot, so they need extern crate.
28extern crate rustc_driver;
29
30fn main() {
31    rustc_driver::install_ice_hook(BUG_REPORT_URL, |_| ());
32
33    tracing_subscriber::fmt()
34        .with_env_filter(EnvFilter::from_env("RUSTFMT_LOG"))
35        .init();
36    let opts = make_opts();
37
38    let exit_code = match execute(&opts) {
39        Ok(code) => code,
40        Err(e) => {
41            eprintln!("{e:#}");
42            1
43        }
44    };
45    // Make sure standard output is flushed before we exit.
46    std::io::stdout().flush().unwrap();
47
48    // Exit with given exit code.
49    //
50    // NOTE: this immediately terminates the process without doing any cleanup,
51    // so make sure to finish all necessary cleanup before this is called.
52    std::process::exit(exit_code);
53}
54
55/// Rustfmt operations.
56enum Operation {
57    /// Format files and their child modules.
58    Format {
59        files: Vec<PathBuf>,
60        minimal_config_path: Option<String>,
61    },
62    /// Print the help message.
63    Help(HelpOp),
64    /// Print version information
65    Version,
66    /// Output default config to a file, or stdout if None
67    ConfigOutputDefault { path: Option<String> },
68    /// Output current config (as if formatting to a file) to stdout
69    ConfigOutputCurrent { path: Option<String> },
70    /// No file specified, read from stdin
71    Stdin { input: String },
72}
73
74/// Rustfmt operations errors.
75#[derive(Error, Debug)]
76pub enum OperationError {
77    /// An unknown help topic was requested.
78    #[error("Unknown help topic: `{0}`.")]
79    UnknownHelpTopic(String),
80    /// An unknown print-config option was requested.
81    #[error("Unknown print-config option: `{0}`.")]
82    UnknownPrintConfigTopic(String),
83    /// Attempt to generate a minimal config from standard input.
84    #[error("The `--print-config=minimal` option doesn't work with standard input.")]
85    MinimalPathWithStdin,
86    /// An io error during reading or writing.
87    #[error("{0}")]
88    IoError(IoError),
89    /// Attempt to use --emit with a mode which is not currently
90    /// supported with standard input.
91    #[error("Emit mode {0} not supported with standard output.")]
92    StdinBadEmit(EmitMode),
93}
94
95impl From<IoError> for OperationError {
96    fn from(e: IoError) -> OperationError {
97        OperationError::IoError(e)
98    }
99}
100
101/// Arguments to `--help`
102enum HelpOp {
103    None,
104    Config,
105    FileLines,
106}
107
108fn make_opts() -> Options {
109    let mut opts = Options::new();
110
111    opts.optflag(
112        "",
113        "check",
114        "Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits \
115         with 1 and prints a diff if formatting is required.",
116    );
117    let is_nightly = is_nightly();
118    let emit_opts = if is_nightly {
119        "[files|stdout|coverage|checkstyle|json]"
120    } else {
121        "[files|stdout]"
122    };
123    opts.optopt("", "emit", "What data to emit and how", emit_opts);
124    opts.optflag("", "backup", "Backup any modified files.");
125    opts.optopt(
126        "",
127        "config-path",
128        "Recursively searches the given path for the rustfmt.toml config file. If not \
129         found reverts to the input file path",
130        "[Path for the configuration file]",
131    );
132    opts.optopt(
133        "",
134        "edition",
135        "Rust edition to use",
136        "[2015|2018|2021|2024]",
137    );
138    opts.optopt(
139        "",
140        "style-edition",
141        "The edition of the Style Guide (unstable).",
142        "[2015|2018|2021|2024]",
143    );
144    opts.optopt(
145        "",
146        "color",
147        "Use colored output (if supported)",
148        "[always|never|auto]",
149    );
150    opts.optopt(
151        "",
152        "print-config",
153        "Dumps a default or minimal config to PATH. A minimal config is the \
154         subset of the current config file used for formatting the current program. \
155         `current` writes to stdout current config as if formatting the file at PATH.",
156        "[default|minimal|current] PATH",
157    );
158    opts.optflag(
159        "l",
160        "files-with-diff",
161        "Prints the names of mismatched files that were formatted. Prints the names of \
162         files that would be formatted when used with `--check` mode. ",
163    );
164    opts.optmulti(
165        "",
166        "config",
167        "Set options from command line. These settings take priority over .rustfmt.toml",
168        "[key1=val1,key2=val2...]",
169    );
170    opts.optopt(
171        "",
172        "style-edition",
173        "The edition of the Style Guide.",
174        "[2015|2018|2021|2024]",
175    );
176
177    if is_nightly {
178        opts.optflag(
179            "",
180            "unstable-features",
181            "Enables unstable features. Only available on nightly channel.",
182        );
183        opts.optopt(
184            "",
185            "file-lines",
186            "Format specified line ranges. Run with `--help=file-lines` for \
187             more detail (unstable).",
188            "JSON",
189        );
190        opts.optflag(
191            "",
192            "error-on-unformatted",
193            "Error if unable to get comments or string literals within max_width, \
194             or they are left with trailing whitespaces (unstable).",
195        );
196        opts.optflag(
197            "",
198            "skip-children",
199            "Don't reformat child modules (unstable).",
200        );
201    }
202
203    opts.optflag("v", "verbose", "Print verbose output");
204    opts.optflag("q", "quiet", "Print less output");
205    opts.optflag("V", "version", "Show version information");
206    let help_topics = if is_nightly {
207        "`config` or `file-lines`"
208    } else {
209        "`config`"
210    };
211    let mut help_topic_msg = "Show this message or help about a specific topic: ".to_owned();
212    help_topic_msg.push_str(help_topics);
213
214    opts.optflagopt("h", "help", &help_topic_msg, "=TOPIC");
215
216    opts
217}
218
219fn is_nightly() -> bool {
220    option_env!("CFG_RELEASE_CHANNEL").map_or(true, |c| c == "nightly" || c == "dev")
221}
222
223// Returned i32 is an exit code
224fn execute(opts: &Options) -> Result<i32> {
225    let matches = opts.parse(env::args().skip(1))?;
226    let options = GetOptsOptions::from_matches(&matches)?;
227
228    match determine_operation(&matches)? {
229        Operation::Help(HelpOp::None) => {
230            print_usage_to_stdout(opts, "");
231            Ok(0)
232        }
233        Operation::Help(HelpOp::Config) => {
234            Config::print_docs(&mut stdout(), options.unstable_features);
235            Ok(0)
236        }
237        Operation::Help(HelpOp::FileLines) => {
238            print_help_file_lines();
239            Ok(0)
240        }
241        Operation::Version => {
242            print_version();
243            Ok(0)
244        }
245        Operation::ConfigOutputDefault { path } => {
246            let toml = Config::default().all_options().to_toml()?;
247            if let Some(path) = path {
248                let mut file = File::create(path)?;
249                file.write_all(toml.as_bytes())?;
250            } else {
251                io::stdout().write_all(toml.as_bytes())?;
252            }
253            Ok(0)
254        }
255        Operation::ConfigOutputCurrent { path } => {
256            let path = match path {
257                Some(path) => path,
258                None => return Err(format_err!("PATH required for `--print-config current`")),
259            };
260
261            let file = PathBuf::from(path);
262            let file = file.canonicalize().unwrap_or(file);
263
264            let (config, _) = load_config(Some(file.parent().unwrap()), Some(options))?;
265            let toml = config.all_options().to_toml()?;
266            io::stdout().write_all(toml.as_bytes())?;
267
268            Ok(0)
269        }
270        Operation::Stdin { input } => format_string(input, options),
271        Operation::Format {
272            files,
273            minimal_config_path,
274        } => format(files, minimal_config_path, &options),
275    }
276}
277
278fn format_string(input: String, options: GetOptsOptions) -> Result<i32> {
279    // try to read config from local directory
280    let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
281
282    if options.check {
283        config.set_cli().emit_mode(EmitMode::Diff);
284    } else {
285        match options.emit_mode {
286            // Emit modes which work with standard input
287            // None means default, which is Stdout.
288            None => {
289                config
290                    .set()
291                    .emit_mode(options.emit_mode.unwrap_or(EmitMode::Stdout));
292            }
293            Some(EmitMode::Stdout) | Some(EmitMode::Checkstyle) | Some(EmitMode::Json) => {
294                config
295                    .set_cli()
296                    .emit_mode(options.emit_mode.unwrap_or(EmitMode::Stdout));
297            }
298            Some(emit_mode) => {
299                return Err(OperationError::StdinBadEmit(emit_mode).into());
300            }
301        }
302    }
303    config.set().verbose(Verbosity::Quiet);
304
305    // parse file_lines
306    if options.file_lines.is_all() {
307        config.set().file_lines(options.file_lines);
308    } else {
309        config.set_cli().file_lines(options.file_lines);
310    }
311
312    for f in config.file_lines().files() {
313        match *f {
314            FileName::Stdin => {}
315            _ => eprintln!("Warning: Extra file listed in file_lines option '{f}'"),
316        }
317    }
318
319    let out = &mut stdout();
320    let mut session = Session::new(config, Some(out));
321    format_and_emit_report(&mut session, Input::Text(input));
322
323    let exit_code = if session.has_operational_errors() || session.has_parsing_errors() {
324        1
325    } else {
326        0
327    };
328    Ok(exit_code)
329}
330
331fn format(
332    files: Vec<PathBuf>,
333    minimal_config_path: Option<String>,
334    options: &GetOptsOptions,
335) -> Result<i32> {
336    options.verify_file_lines(&files);
337    let (config, config_path) = load_config(None, Some(options.clone()))?;
338
339    if config.verbose() == Verbosity::Verbose {
340        if let Some(path) = config_path.as_ref() {
341            println!("Using rustfmt config file {}", path.display());
342        }
343    }
344
345    let out = &mut stdout();
346    let mut session = Session::new(config, Some(out));
347
348    for file in files {
349        if !file.exists() {
350            eprintln!("Error: file `{}` does not exist", file.display());
351            session.add_operational_error();
352        } else if file.is_dir() {
353            eprintln!("Error: `{}` is a directory", file.display());
354            session.add_operational_error();
355        } else {
356            // Check the file directory if the config-path could not be read or not provided
357            if config_path.is_none() {
358                let (local_config, config_path) =
359                    load_config(Some(file.parent().unwrap()), Some(options.clone()))?;
360                if local_config.verbose() == Verbosity::Verbose {
361                    if let Some(path) = config_path {
362                        println!(
363                            "Using rustfmt config file {} for {}",
364                            path.display(),
365                            file.display()
366                        );
367                    }
368                }
369
370                session.override_config(local_config, |sess| {
371                    format_and_emit_report(sess, Input::File(file))
372                });
373            } else {
374                format_and_emit_report(&mut session, Input::File(file));
375            }
376        }
377    }
378
379    // If we were given a path via dump-minimal-config, output any options
380    // that were used during formatting as TOML.
381    if let Some(path) = minimal_config_path {
382        let mut file = File::create(path)?;
383        let toml = session.config.used_options().to_toml()?;
384        file.write_all(toml.as_bytes())?;
385    }
386
387    let exit_code = if session.has_operational_errors()
388        || session.has_parsing_errors()
389        || ((session.has_diff() || session.has_check_errors()) && options.check)
390    {
391        1
392    } else {
393        0
394    };
395    Ok(exit_code)
396}
397
398fn format_and_emit_report<T: Write>(session: &mut Session<'_, T>, input: Input) {
399    match session.format(input) {
400        Ok(report) => {
401            if report.has_warnings() {
402                eprintln!(
403                    "{}",
404                    FormatReportFormatterBuilder::new(&report)
405                        .enable_colors(should_print_with_colors(session))
406                        .build()
407                );
408            }
409        }
410        Err(msg) => {
411            eprintln!("Error writing files: {msg}");
412            session.add_operational_error();
413        }
414    }
415}
416
417fn should_print_with_colors<T: Write>(session: &mut Session<'_, T>) -> bool {
418    term::stderr().is_some_and(|t| {
419        session.config.color().use_colored_tty()
420            && t.supports_color()
421            && t.supports_attr(term::Attr::Bold)
422    })
423}
424
425fn print_usage_to_stdout(opts: &Options, reason: &str) {
426    let sep = if reason.is_empty() {
427        String::new()
428    } else {
429        format!("{reason}\n\n")
430    };
431    let msg = format!("{sep}Format Rust code\n\nusage: rustfmt [options] <file>...");
432    println!("{}", opts.usage(&msg));
433}
434
435fn print_help_file_lines() {
436    println!(
437        "If you want to restrict reformatting to specific sets of lines, you can
438use the `--file-lines` option. Its argument is a JSON array of objects
439with `file` and `range` properties, where `file` is a file name, and
440`range` is an array representing a range of lines like `[7,13]`. Ranges
441are 1-based and inclusive of both end points. Specifying an empty array
442will result in no files being formatted. For example,
443
444```
445rustfmt src/lib.rs src/foo.rs --file-lines '[
446    {{\"file\":\"src/lib.rs\",\"range\":[7,13]}},
447    {{\"file\":\"src/lib.rs\",\"range\":[21,29]}},
448    {{\"file\":\"src/foo.rs\",\"range\":[10,11]}},
449    {{\"file\":\"src/foo.rs\",\"range\":[15,15]}}]'
450```
451
452would format lines `7-13` and `21-29` of `src/lib.rs`, and lines `10-11`,
453and `15` of `src/foo.rs`. No other files would be formatted, even if they
454are included as out of line modules from `src/lib.rs`."
455    );
456}
457
458fn print_version() {
459    let version_number = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
460    let commit_info = include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"));
461
462    if commit_info.is_empty() {
463        println!("rustfmt {version_number}");
464    } else {
465        println!("rustfmt {version_number}-{commit_info}");
466    }
467}
468
469fn determine_operation(matches: &Matches) -> Result<Operation, OperationError> {
470    if matches.opt_present("h") {
471        let Some(topic) = matches.opt_str("h") else {
472            return Ok(Operation::Help(HelpOp::None));
473        };
474
475        return match topic.as_str() {
476            "config" => Ok(Operation::Help(HelpOp::Config)),
477            "file-lines" if is_nightly() => Ok(Operation::Help(HelpOp::FileLines)),
478            _ => Err(OperationError::UnknownHelpTopic(topic)),
479        };
480    }
481    let mut free_matches = matches.free.iter();
482
483    let mut minimal_config_path = None;
484    if let Some(kind) = matches.opt_str("print-config") {
485        let path = free_matches.next().cloned();
486        match kind.as_str() {
487            "default" => return Ok(Operation::ConfigOutputDefault { path }),
488            "current" => return Ok(Operation::ConfigOutputCurrent { path }),
489            "minimal" => {
490                minimal_config_path = path;
491                if minimal_config_path.is_none() {
492                    eprintln!("WARNING: PATH required for `--print-config minimal`.");
493                }
494            }
495            _ => {
496                return Err(OperationError::UnknownPrintConfigTopic(kind));
497            }
498        }
499    }
500
501    if matches.opt_present("version") {
502        return Ok(Operation::Version);
503    }
504
505    let files: Vec<_> = free_matches
506        .map(|s| {
507            let p = PathBuf::from(s);
508            // we will do comparison later, so here tries to canonicalize first
509            // to get the expected behavior.
510            p.canonicalize().unwrap_or(p)
511        })
512        .collect();
513
514    // if no file argument is supplied, read from stdin
515    if files.is_empty() {
516        if minimal_config_path.is_some() {
517            return Err(OperationError::MinimalPathWithStdin);
518        }
519        let mut buffer = String::new();
520        io::stdin().read_to_string(&mut buffer)?;
521
522        return Ok(Operation::Stdin { input: buffer });
523    }
524
525    Ok(Operation::Format {
526        files,
527        minimal_config_path,
528    })
529}
530
531const STABLE_EMIT_MODES: [EmitMode; 3] = [EmitMode::Files, EmitMode::Stdout, EmitMode::Diff];
532
533/// Parsed command line options.
534#[derive(Clone, Debug, Default)]
535struct GetOptsOptions {
536    skip_children: Option<bool>,
537    quiet: bool,
538    verbose: bool,
539    config_path: Option<PathBuf>,
540    inline_config: HashMap<String, String>,
541    emit_mode: Option<EmitMode>,
542    backup: bool,
543    check: bool,
544    edition: Option<Edition>,
545    style_edition: Option<StyleEdition>,
546    color: Option<Color>,
547    file_lines: FileLines, // Default is all lines in all files.
548    unstable_features: bool,
549    error_on_unformatted: Option<bool>,
550    print_misformatted_file_names: bool,
551}
552
553impl GetOptsOptions {
554    pub fn from_matches(matches: &Matches) -> Result<GetOptsOptions> {
555        let mut options = GetOptsOptions::default();
556        options.verbose = matches.opt_present("verbose");
557        options.quiet = matches.opt_present("quiet");
558        if options.verbose && options.quiet {
559            return Err(format_err!("Can't use both `--verbose` and `--quiet`"));
560        }
561
562        let rust_nightly = is_nightly();
563
564        if rust_nightly {
565            options.unstable_features = matches.opt_present("unstable-features");
566
567            if options.unstable_features {
568                if matches.opt_present("skip-children") {
569                    options.skip_children = Some(true);
570                }
571                if matches.opt_present("error-on-unformatted") {
572                    options.error_on_unformatted = Some(true);
573                }
574                if let Some(ref file_lines) = matches.opt_str("file-lines") {
575                    options.file_lines = file_lines.parse()?;
576                }
577            } else {
578                let mut unstable_options = vec![];
579                if matches.opt_present("skip-children") {
580                    unstable_options.push("`--skip-children`");
581                }
582                if matches.opt_present("error-on-unformatted") {
583                    unstable_options.push("`--error-on-unformatted`");
584                }
585                if matches.opt_present("file-lines") {
586                    unstable_options.push("`--file-lines`");
587                }
588                if !unstable_options.is_empty() {
589                    let s = if unstable_options.len() == 1 { "" } else { "s" };
590                    return Err(format_err!(
591                        "Unstable option{} ({}) used without `--unstable-features`",
592                        s,
593                        unstable_options.join(", "),
594                    ));
595                }
596            }
597        }
598
599        options.config_path = matches.opt_str("config-path").map(PathBuf::from);
600
601        options.inline_config = matches
602            .opt_strs("config")
603            .iter()
604            .flat_map(|config| config.split(','))
605            .map(
606                |key_val| match key_val.char_indices().find(|(_, ch)| *ch == '=') {
607                    Some((middle, _)) => {
608                        let (key, val) = (&key_val[..middle], &key_val[middle + 1..]);
609                        if !Config::is_valid_key_val(key, val) {
610                            Err(format_err!("invalid key=val pair: `{}`", key_val))
611                        } else {
612                            Ok((key.to_string(), val.to_string()))
613                        }
614                    }
615
616                    None => Err(format_err!(
617                        "--config expects comma-separated list of key=val pairs, found `{}`",
618                        key_val
619                    )),
620                },
621            )
622            .collect::<Result<HashMap<_, _>, _>>()?;
623
624        options.check = matches.opt_present("check");
625        if let Some(ref emit_str) = matches.opt_str("emit") {
626            if options.check {
627                return Err(format_err!("Invalid to use `--emit` and `--check`"));
628            }
629
630            options.emit_mode = Some(emit_mode_from_emit_str(emit_str)?);
631        }
632
633        if let Some(ref edition_str) = matches.opt_str("edition") {
634            options.edition = Some(edition_from_edition_str(edition_str)?);
635        }
636
637        if let Some(ref edition_str) = matches.opt_str("style-edition") {
638            options.style_edition = Some(style_edition_from_style_edition_str(edition_str)?);
639        }
640
641        if matches.opt_present("backup") {
642            options.backup = true;
643        }
644
645        if matches.opt_present("files-with-diff") {
646            options.print_misformatted_file_names = true;
647        }
648
649        if !rust_nightly {
650            if let Some(ref emit_mode) = options.emit_mode {
651                if !STABLE_EMIT_MODES.contains(emit_mode) {
652                    return Err(format_err!(
653                        "Invalid value for `--emit` - using an unstable \
654                         value without `--unstable-features`",
655                    ));
656                }
657            }
658        }
659
660        if let Some(ref color) = matches.opt_str("color") {
661            match Color::from_str(color) {
662                Ok(color) => options.color = Some(color),
663                _ => return Err(format_err!("Invalid color: {}", color)),
664            }
665        }
666
667        if let Some(ref edition_str) = matches.opt_str("style-edition") {
668            options.style_edition = Some(style_edition_from_style_edition_str(edition_str)?);
669        }
670
671        Ok(options)
672    }
673
674    fn verify_file_lines(&self, files: &[PathBuf]) {
675        for f in self.file_lines.files() {
676            match *f {
677                FileName::Real(ref f) if files.contains(f) => {}
678                FileName::Real(_) => {
679                    eprintln!("Warning: Extra file listed in file_lines option '{f}'")
680                }
681                FileName::Stdin => eprintln!("Warning: Not a file '{f}'"),
682            }
683        }
684    }
685}
686
687impl CliOptions for GetOptsOptions {
688    fn apply_to(self, config: &mut Config) {
689        if self.verbose {
690            config.set_cli().verbose(Verbosity::Verbose);
691        } else if self.quiet {
692            config.set_cli().verbose(Verbosity::Quiet);
693        } else {
694            config.set().verbose(Verbosity::Normal);
695        }
696
697        if self.file_lines.is_all() {
698            config.set().file_lines(self.file_lines);
699        } else {
700            config.set_cli().file_lines(self.file_lines);
701        }
702
703        if self.unstable_features {
704            config.set_cli().unstable_features(self.unstable_features);
705        } else {
706            config.set().unstable_features(self.unstable_features);
707        }
708        if let Some(skip_children) = self.skip_children {
709            config.set_cli().skip_children(skip_children);
710        }
711        if let Some(error_on_unformatted) = self.error_on_unformatted {
712            config.set_cli().error_on_unformatted(error_on_unformatted);
713        }
714        if let Some(edition) = self.edition {
715            config.set_cli().edition(edition);
716        }
717        if let Some(edition) = self.style_edition {
718            config.set_cli().style_edition(edition);
719        }
720        if self.check {
721            config.set_cli().emit_mode(EmitMode::Diff);
722        } else if let Some(emit_mode) = self.emit_mode {
723            config.set_cli().emit_mode(emit_mode);
724        }
725        if self.backup {
726            config.set_cli().make_backup(true);
727        }
728        if let Some(color) = self.color {
729            config.set_cli().color(color);
730        }
731        if self.print_misformatted_file_names {
732            config.set_cli().print_misformatted_file_names(true);
733        }
734
735        for (key, val) in self.inline_config {
736            config.override_value(&key, &val);
737        }
738    }
739
740    fn config_path(&self) -> Option<&Path> {
741        self.config_path.as_deref()
742    }
743
744    fn edition(&self) -> Option<Edition> {
745        self.inline_config
746            .get("edition")
747            .map_or(self.edition, |e| Edition::from_str(e).ok())
748    }
749
750    fn style_edition(&self) -> Option<StyleEdition> {
751        self.inline_config
752            .get("style_edition")
753            .map_or(self.style_edition, |se| StyleEdition::from_str(se).ok())
754    }
755
756    fn version(&self) -> Option<Version> {
757        self.inline_config
758            .get("version")
759            .map(|version| Version::from_str(version).ok())
760            .flatten()
761    }
762}
763
764fn edition_from_edition_str(edition_str: &str) -> Result<Edition> {
765    match edition_str {
766        "2015" => Ok(Edition::Edition2015),
767        "2018" => Ok(Edition::Edition2018),
768        "2021" => Ok(Edition::Edition2021),
769        "2024" => Ok(Edition::Edition2024),
770        _ => Err(format_err!("Invalid value for `--edition`")),
771    }
772}
773
774fn style_edition_from_style_edition_str(edition_str: &str) -> Result<StyleEdition> {
775    match edition_str {
776        "2015" => Ok(StyleEdition::Edition2015),
777        "2018" => Ok(StyleEdition::Edition2018),
778        "2021" => Ok(StyleEdition::Edition2021),
779        "2024" => Ok(StyleEdition::Edition2024),
780        "2027" => Ok(StyleEdition::Edition2027),
781        _ => Err(format_err!("Invalid value for `--style-edition`")),
782    }
783}
784
785fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode> {
786    match emit_str {
787        "files" => Ok(EmitMode::Files),
788        "stdout" => Ok(EmitMode::Stdout),
789        "coverage" => Ok(EmitMode::Coverage),
790        "checkstyle" => Ok(EmitMode::Checkstyle),
791        "json" => Ok(EmitMode::Json),
792        _ => Err(format_err!("Invalid value for `--emit`")),
793    }
794}
795
796#[cfg(test)]
797#[allow(dead_code)]
798mod test {
799    use super::*;
800    use rustfmt_config_proc_macro::nightly_only_test;
801
802    fn get_config<O: CliOptions>(path: Option<&Path>, options: Option<O>) -> Config {
803        load_config(path, options).unwrap().0
804    }
805
806    #[nightly_only_test]
807    #[test]
808    fn flag_sets_style_edition_override_correctly() {
809        let mut options = GetOptsOptions::default();
810        options.style_edition = Some(StyleEdition::Edition2024);
811        let config = get_config(None, Some(options));
812        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
813    }
814
815    #[nightly_only_test]
816    #[test]
817    fn edition_sets_style_edition_override_correctly() {
818        let mut options = GetOptsOptions::default();
819        options.edition = Some(Edition::Edition2024);
820        let config = get_config(None, Some(options));
821        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
822    }
823
824    #[nightly_only_test]
825    #[test]
826    fn version_sets_style_edition_override_correctly() {
827        let mut options = GetOptsOptions::default();
828        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
829        let config = get_config(None, Some(options));
830        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
831    }
832
833    #[nightly_only_test]
834    #[test]
835    fn version_config_file_sets_style_edition_override_correctly() {
836        let options = GetOptsOptions::default();
837        let config_file = Some(Path::new("tests/config/style-edition/just-version"));
838        let config = get_config(config_file, Some(options));
839        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
840    }
841
842    #[nightly_only_test]
843    #[test]
844    fn style_edition_flag_has_correct_precedence_over_edition() {
845        let mut options = GetOptsOptions::default();
846        options.style_edition = Some(StyleEdition::Edition2021);
847        options.edition = Some(Edition::Edition2024);
848        let config = get_config(None, Some(options));
849        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
850    }
851
852    #[nightly_only_test]
853    #[test]
854    fn style_edition_flag_has_correct_precedence_over_version() {
855        let mut options = GetOptsOptions::default();
856        options.style_edition = Some(StyleEdition::Edition2018);
857        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
858        let config = get_config(None, Some(options));
859        assert_eq!(config.style_edition(), StyleEdition::Edition2018);
860    }
861
862    #[nightly_only_test]
863    #[test]
864    fn style_edition_flag_has_correct_precedence_over_edition_version() {
865        let mut options = GetOptsOptions::default();
866        options.style_edition = Some(StyleEdition::Edition2021);
867        options.edition = Some(Edition::Edition2018);
868        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
869        let config = get_config(None, Some(options));
870        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
871    }
872
873    #[nightly_only_test]
874    #[test]
875    fn style_edition_inline_has_correct_precedence_over_edition_version() {
876        let mut options = GetOptsOptions::default();
877        options.edition = Some(Edition::Edition2018);
878        options.inline_config = HashMap::from([
879            ("version".to_owned(), "One".to_owned()),
880            ("style_edition".to_owned(), "2024".to_owned()),
881        ]);
882        let config = get_config(None, Some(options));
883        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
884    }
885
886    #[nightly_only_test]
887    #[test]
888    fn style_edition_config_file_trumps_edition_flag_version_inline() {
889        let mut options = GetOptsOptions::default();
890        let config_file = Some(Path::new("tests/config/style-edition/just-style-edition"));
891        options.edition = Some(Edition::Edition2018);
892        options.inline_config = HashMap::from([("version".to_owned(), "One".to_owned())]);
893        let config = get_config(config_file, Some(options));
894        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
895    }
896
897    #[nightly_only_test]
898    #[test]
899    fn style_edition_config_file_trumps_edition_config_and_version_inline() {
900        let mut options = GetOptsOptions::default();
901        let config_file = Some(Path::new(
902            "tests/config/style-edition/style-edition-and-edition",
903        ));
904        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
905        let config = get_config(config_file, Some(options));
906        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
907        assert_eq!(config.edition(), Edition::Edition2024);
908    }
909
910    #[nightly_only_test]
911    #[test]
912    fn version_config_trumps_edition_config_and_flag() {
913        let mut options = GetOptsOptions::default();
914        let config_file = Some(Path::new("tests/config/style-edition/version-edition"));
915        options.edition = Some(Edition::Edition2018);
916        let config = get_config(config_file, Some(options));
917        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
918    }
919
920    #[nightly_only_test]
921    #[test]
922    fn style_edition_config_file_trumps_version_config() {
923        let options = GetOptsOptions::default();
924        let config_file = Some(Path::new(
925            "tests/config/style-edition/version-style-edition",
926        ));
927        let config = get_config(config_file, Some(options));
928        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
929    }
930
931    #[nightly_only_test]
932    #[test]
933    fn style_edition_config_file_trumps_edition_version_config() {
934        let options = GetOptsOptions::default();
935        let config_file = Some(Path::new(
936            "tests/config/style-edition/version-style-edition-and-edition",
937        ));
938        let config = get_config(config_file, Some(options));
939        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
940    }
941
942    #[nightly_only_test]
943    #[test]
944    fn correct_defaults_for_style_edition_loaded() {
945        let mut options = GetOptsOptions::default();
946        options.style_edition = Some(StyleEdition::Edition2024);
947        let config = get_config(None, Some(options));
948        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
949    }
950
951    #[nightly_only_test]
952    #[test]
953    fn style_edition_defaults_overridden_from_config() {
954        let options = GetOptsOptions::default();
955        let config_file = Some(Path::new("tests/config/style-edition/overrides"));
956        let config = get_config(config_file, Some(options));
957        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
958        // FIXME: this test doesn't really exercise anything, since
959        // `overflow_delimited_expr` is disabled by default in edition 2024.
960        assert_eq!(config.overflow_delimited_expr(), false);
961    }
962
963    #[nightly_only_test]
964    #[test]
965    fn style_edition_defaults_overridden_from_cli() {
966        let mut options = GetOptsOptions::default();
967        let config_file = Some(Path::new("tests/config/style-edition/just-style-edition"));
968        options.inline_config =
969            HashMap::from([("overflow_delimited_expr".to_owned(), "true".to_owned())]);
970        let config = get_config(config_file, Some(options));
971        // FIXME: this test doesn't really exercise anything, since
972        // `overflow_delimited_expr` is disabled by default in edition 2024.
973        assert_eq!(config.overflow_delimited_expr(), true);
974    }
975}