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