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 let Some(ref edition_str) = matches.opt_str("edition") {
628            options.edition = Some(edition_from_edition_str(edition_str)?);
629        }
630
631        if matches.opt_present("backup") {
632            options.backup = true;
633        }
634
635        if matches.opt_present("files-with-diff") {
636            options.print_misformatted_file_names = true;
637        }
638
639        if !rust_nightly {
640            if let Some(ref emit_mode) = options.emit_mode {
641                if !STABLE_EMIT_MODES.contains(emit_mode) {
642                    return Err(format_err!(
643                        "Invalid value for `--emit` - using an unstable \
644                         value without `--unstable-features`",
645                    ));
646                }
647            }
648        }
649
650        if let Some(ref color) = matches.opt_str("color") {
651            match Color::from_str(color) {
652                Ok(color) => options.color = Some(color),
653                _ => return Err(format_err!("Invalid color: {}", color)),
654            }
655        }
656
657        if let Some(ref edition_str) = matches.opt_str("style-edition") {
658            options.style_edition = Some(style_edition_from_style_edition_str(edition_str)?);
659        }
660
661        Ok(options)
662    }
663
664    fn verify_file_lines(&self, files: &[PathBuf]) {
665        for f in self.file_lines.files() {
666            match *f {
667                FileName::Real(ref f) if files.contains(f) => {}
668                FileName::Real(_) => {
669                    eprintln!("Warning: Extra file listed in file_lines option '{f}'")
670                }
671                FileName::Stdin => eprintln!("Warning: Not a file '{f}'"),
672            }
673        }
674    }
675}
676
677impl CliOptions for GetOptsOptions {
678    fn apply_to(self, config: &mut Config) {
679        if self.verbose {
680            config.set_cli().verbose(Verbosity::Verbose);
681        } else if self.quiet {
682            config.set_cli().verbose(Verbosity::Quiet);
683        } else {
684            config.set().verbose(Verbosity::Normal);
685        }
686
687        if self.file_lines.is_all() {
688            config.set().file_lines(self.file_lines);
689        } else {
690            config.set_cli().file_lines(self.file_lines);
691        }
692
693        if self.unstable_features {
694            config.set_cli().unstable_features(self.unstable_features);
695        } else {
696            config.set().unstable_features(self.unstable_features);
697        }
698        if let Some(skip_children) = self.skip_children {
699            config.set_cli().skip_children(skip_children);
700        }
701        if let Some(error_on_unformatted) = self.error_on_unformatted {
702            config.set_cli().error_on_unformatted(error_on_unformatted);
703        }
704        if let Some(edition) = self.edition {
705            config.set_cli().edition(edition);
706        }
707        if let Some(edition) = self.style_edition {
708            config.set_cli().style_edition(edition);
709        }
710        if self.check {
711            config.set_cli().emit_mode(EmitMode::Diff);
712        } else if let Some(emit_mode) = self.emit_mode {
713            config.set_cli().emit_mode(emit_mode);
714        }
715        if self.backup {
716            config.set_cli().make_backup(true);
717        }
718        if let Some(color) = self.color {
719            config.set_cli().color(color);
720        }
721        if self.print_misformatted_file_names {
722            config.set_cli().print_misformatted_file_names(true);
723        }
724
725        for (key, val) in self.inline_config {
726            config.override_value(&key, &val);
727        }
728    }
729
730    fn config_path(&self) -> Option<&Path> {
731        self.config_path.as_deref()
732    }
733
734    fn edition(&self) -> Option<Edition> {
735        self.inline_config
736            .get("edition")
737            .map_or(self.edition, |e| Edition::from_str(e).ok())
738    }
739
740    fn style_edition(&self) -> Option<StyleEdition> {
741        self.inline_config
742            .get("style_edition")
743            .map_or(self.style_edition, |se| StyleEdition::from_str(se).ok())
744    }
745
746    fn version(&self) -> Option<Version> {
747        self.inline_config
748            .get("version")
749            .map(|version| Version::from_str(version).ok())
750            .flatten()
751    }
752}
753
754fn edition_from_edition_str(edition_str: &str) -> Result<Edition> {
755    match edition_str {
756        "2015" => Ok(Edition::Edition2015),
757        "2018" => Ok(Edition::Edition2018),
758        "2021" => Ok(Edition::Edition2021),
759        "2024" => Ok(Edition::Edition2024),
760        _ => Err(format_err!("Invalid value for `--edition`")),
761    }
762}
763
764fn style_edition_from_style_edition_str(edition_str: &str) -> Result<StyleEdition> {
765    match edition_str {
766        "2015" => Ok(StyleEdition::Edition2015),
767        "2018" => Ok(StyleEdition::Edition2018),
768        "2021" => Ok(StyleEdition::Edition2021),
769        "2024" => Ok(StyleEdition::Edition2024),
770        _ => Err(format_err!("Invalid value for `--style-edition`")),
771    }
772}
773
774fn emit_mode_from_emit_str(emit_str: &str) -> Result<EmitMode> {
775    match emit_str {
776        "files" => Ok(EmitMode::Files),
777        "stdout" => Ok(EmitMode::Stdout),
778        "coverage" => Ok(EmitMode::Coverage),
779        "checkstyle" => Ok(EmitMode::Checkstyle),
780        "json" => Ok(EmitMode::Json),
781        _ => Err(format_err!("Invalid value for `--emit`")),
782    }
783}
784
785#[cfg(test)]
786#[allow(dead_code)]
787mod test {
788    use super::*;
789    use rustfmt_config_proc_macro::nightly_only_test;
790
791    fn get_config<O: CliOptions>(path: Option<&Path>, options: Option<O>) -> Config {
792        load_config(path, options).unwrap().0
793    }
794
795    #[nightly_only_test]
796    #[test]
797    fn flag_sets_style_edition_override_correctly() {
798        let mut options = GetOptsOptions::default();
799        options.style_edition = Some(StyleEdition::Edition2024);
800        let config = get_config(None, Some(options));
801        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
802    }
803
804    #[nightly_only_test]
805    #[test]
806    fn edition_sets_style_edition_override_correctly() {
807        let mut options = GetOptsOptions::default();
808        options.edition = Some(Edition::Edition2024);
809        let config = get_config(None, Some(options));
810        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
811    }
812
813    #[nightly_only_test]
814    #[test]
815    fn version_sets_style_edition_override_correctly() {
816        let mut options = GetOptsOptions::default();
817        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
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 version_config_file_sets_style_edition_override_correctly() {
825        let options = GetOptsOptions::default();
826        let config_file = Some(Path::new("tests/config/style-edition/just-version"));
827        let config = get_config(config_file, Some(options));
828        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
829    }
830
831    #[nightly_only_test]
832    #[test]
833    fn style_edition_flag_has_correct_precedence_over_edition() {
834        let mut options = GetOptsOptions::default();
835        options.style_edition = Some(StyleEdition::Edition2021);
836        options.edition = Some(Edition::Edition2024);
837        let config = get_config(None, Some(options));
838        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
839    }
840
841    #[nightly_only_test]
842    #[test]
843    fn style_edition_flag_has_correct_precedence_over_version() {
844        let mut options = GetOptsOptions::default();
845        options.style_edition = Some(StyleEdition::Edition2018);
846        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
847        let config = get_config(None, Some(options));
848        assert_eq!(config.style_edition(), StyleEdition::Edition2018);
849    }
850
851    #[nightly_only_test]
852    #[test]
853    fn style_edition_flag_has_correct_precedence_over_edition_version() {
854        let mut options = GetOptsOptions::default();
855        options.style_edition = Some(StyleEdition::Edition2021);
856        options.edition = Some(Edition::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::Edition2021);
860    }
861
862    #[nightly_only_test]
863    #[test]
864    fn style_edition_inline_has_correct_precedence_over_edition_version() {
865        let mut options = GetOptsOptions::default();
866        options.edition = Some(Edition::Edition2018);
867        options.inline_config = HashMap::from([
868            ("version".to_owned(), "One".to_owned()),
869            ("style_edition".to_owned(), "2024".to_owned()),
870        ]);
871        let config = get_config(None, Some(options));
872        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
873    }
874
875    #[nightly_only_test]
876    #[test]
877    fn style_edition_config_file_trumps_edition_flag_version_inline() {
878        let mut options = GetOptsOptions::default();
879        let config_file = Some(Path::new("tests/config/style-edition/just-style-edition"));
880        options.edition = Some(Edition::Edition2018);
881        options.inline_config = HashMap::from([("version".to_owned(), "One".to_owned())]);
882        let config = get_config(config_file, 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_config_and_version_inline() {
889        let mut options = GetOptsOptions::default();
890        let config_file = Some(Path::new(
891            "tests/config/style-edition/style-edition-and-edition",
892        ));
893        options.inline_config = HashMap::from([("version".to_owned(), "Two".to_owned())]);
894        let config = get_config(config_file, Some(options));
895        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
896        assert_eq!(config.edition(), Edition::Edition2024);
897    }
898
899    #[nightly_only_test]
900    #[test]
901    fn version_config_trumps_edition_config_and_flag() {
902        let mut options = GetOptsOptions::default();
903        let config_file = Some(Path::new("tests/config/style-edition/version-edition"));
904        options.edition = Some(Edition::Edition2018);
905        let config = get_config(config_file, Some(options));
906        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
907    }
908
909    #[nightly_only_test]
910    #[test]
911    fn style_edition_config_file_trumps_version_config() {
912        let options = GetOptsOptions::default();
913        let config_file = Some(Path::new(
914            "tests/config/style-edition/version-style-edition",
915        ));
916        let config = get_config(config_file, Some(options));
917        assert_eq!(config.style_edition(), StyleEdition::Edition2021);
918    }
919
920    #[nightly_only_test]
921    #[test]
922    fn style_edition_config_file_trumps_edition_version_config() {
923        let options = GetOptsOptions::default();
924        let config_file = Some(Path::new(
925            "tests/config/style-edition/version-style-edition-and-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 correct_defaults_for_style_edition_loaded() {
934        let mut options = GetOptsOptions::default();
935        options.style_edition = Some(StyleEdition::Edition2024);
936        let config = get_config(None, Some(options));
937        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
938    }
939
940    #[nightly_only_test]
941    #[test]
942    fn style_edition_defaults_overridden_from_config() {
943        let options = GetOptsOptions::default();
944        let config_file = Some(Path::new("tests/config/style-edition/overrides"));
945        let config = get_config(config_file, Some(options));
946        assert_eq!(config.style_edition(), StyleEdition::Edition2024);
947        // FIXME: this test doesn't really exercise anything, since
948        // `overflow_delimited_expr` is disabled by default in edition 2024.
949        assert_eq!(config.overflow_delimited_expr(), false);
950    }
951
952    #[nightly_only_test]
953    #[test]
954    fn style_edition_defaults_overridden_from_cli() {
955        let mut options = GetOptsOptions::default();
956        let config_file = Some(Path::new("tests/config/style-edition/just-style-edition"));
957        options.inline_config =
958            HashMap::from([("overflow_delimited_expr".to_owned(), "false".to_owned())]);
959        let config = get_config(config_file, Some(options));
960        // FIXME: this test doesn't really exercise anything, since
961        // `overflow_delimited_expr` is disabled by default in edition 2024.
962        assert_eq!(config.overflow_delimited_expr(), false);
963    }
964}