diff --git a/.gitattributes b/.gitattributes index 91df4f0eb18..b08c45648d9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ tests/source/configs/enum_discrim_align_threshold/40.rs -text tests/target/issue-3494/crlf.rs -text tests/target/comment_crlf_newline.rs -text tests/target/configs/enum_discrim_align_threshold/40.rs -text +tests/editorconfig/target/eol-crlf/main.rs -text diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6d9a3b8dc..938a137a617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ ## [Unreleased] +### Added +- rustfmt now has unstable support for [.editorconfig](https://editorconfig.org/), enabled with `--unstable-features --use-editorconfig`. + The following `.editorconfig` options are respected when applied to Rust source files: + + ```ini + [*.rs] + # These three options cannot be modified, as they do not have corresponding rustfmt options. + # If they are present at all, they are required to take on certain values: + # Required to be utf-8 or utf-8-bom. + charset = utf-8 + # Required to be true. + trim_trailing_whitespace = true + # Required to be true. + insert_final_newline = true + + # These options correspond to existing stable rustfmt options. + # Corresponds to hard_tabs (spaces => false, tabs => true). + indent_style = spaces + # Corresponds to tab_spaces. + indent_size = 3 + # Alternatively, this combination sets tab_spaces via tab_width: + indent_size = tab + tab_width = 3 + # Corresponds to newline_style (lf => "unix", crlf => "windows", cr is unsupported). + newline_style = lf + # Corresponds to max_width. + max_line_len = 100 + ``` + + Note that only the configuration for the entry point (e.g. `main.rs`, `lib.rs`, or integration tests) is respected, but not for child modules. + Use a `[*.rs]` section in your .editorconfig to avoid surprises. + There is no dedicated support in `cargo-fmt`; use `cargo fmt -- --unstable-features --use-editorconfig` to pass the option through to rustfmt. ## [1.10.0] 2026-07-21 diff --git a/Cargo.lock b/Cargo.lock index 9ba1ded57c8..45bc8d871e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,6 +251,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ec4rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b31a881d38439026e3d5dd938ab20328d36e23caca8fd5981c42e4b677f5842" + [[package]] name = "either" version = "1.6.1" @@ -583,6 +589,7 @@ dependencies = [ "clap-cargo", "diff", "dirs", + "ec4rs", "getopts", "ignore", "insta", diff --git a/Cargo.toml b/Cargo.toml index 7e4d2e60565..2cca17913d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ clap = { version = "4.4.2", features = ["derive"] } clap-cargo = "0.18.3" diff = "0.1" dirs = "6.0" +ec4rs = "1.2" getopts = "0.2" ignore = "0.4" itertools = "0.15" diff --git a/src/bin/main.rs b/src/bin/main.rs index 05a22324d0f..9e6a3771b62 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -192,6 +192,11 @@ fn make_opts() -> Options { "skip-children", "Don't reformat child modules (unstable).", ); + opts.optflag( + "", + "use-editorconfig", + "Respect .editorconfig settings for Rust source files.", + ); } opts.optflag("v", "verbose", "Print verbose output"); @@ -255,7 +260,7 @@ fn execute(opts: &Options) -> Result { let file = PathBuf::from(path); let file = file.canonicalize().unwrap_or(file); - let (config, _) = load_config(Some(file.parent().unwrap()), Some(options))?; + let (config, _) = load_config(Some(&file), Some(options))?; let toml = config.all_options().to_toml()?; io::stdout().write_all(toml.as_bytes())?; @@ -349,8 +354,7 @@ fn format( } else { // Check the file directory if the config-path could not be read or not provided if config_path.is_none() { - let (local_config, config_path) = - load_config(Some(file.parent().unwrap()), Some(options.clone()))?; + let (local_config, config_path) = load_config(Some(&file), Some(options.clone()))?; if local_config.verbose() == Verbosity::Verbose { if let Some(path) = config_path { println!( @@ -531,6 +535,7 @@ struct GetOptsOptions { quiet: bool, verbose: bool, config_path: Option, + use_editorconfig: bool, inline_config: HashMap, emit_mode: Option, backup: bool, @@ -568,6 +573,9 @@ impl GetOptsOptions { if let Some(ref file_lines) = matches.opt_str("file-lines") { options.file_lines = file_lines.parse()?; } + if matches.opt_present("use-editorconfig") { + options.use_editorconfig = true; + } } else { let mut unstable_options = vec![]; if matches.opt_present("skip-children") { @@ -579,6 +587,9 @@ impl GetOptsOptions { if matches.opt_present("file-lines") { unstable_options.push("`--file-lines`"); } + if matches.opt_present("use-editorconfig") { + unstable_options.push("`--use-editorconfig`"); + } if !unstable_options.is_empty() { let s = if unstable_options.len() == 1 { "" } else { "s" }; return Err(format_err!( @@ -762,6 +773,10 @@ impl CliOptions for GetOptsOptions { .map(|version| Version::from_str(version).ok()) .flatten() } + + fn use_editorconfig(&self) -> bool { + self.unstable_features && self.use_editorconfig + } } fn edition_from_edition_str(edition_str: &str) -> Result { diff --git a/src/config/config_type.rs b/src/config/config_type.rs index f559cc0d620..9508feb6ef0 100644 --- a/src/config/config_type.rs +++ b/src/config/config_type.rs @@ -100,7 +100,7 @@ macro_rules! create_config { // specify all properties of `Config`. // We first parse into `PartialConfig`, then create a default `Config` // and overwrite the properties with corresponding values from `PartialConfig`. - #[derive(Deserialize, Serialize, Clone)] + #[derive(Deserialize, Serialize, Clone, Default)] #[allow(unreachable_pub)] pub struct PartialConfig { $(pub $i: Option<<$ty as StyleEditionDefault>::ConfigType>),+ @@ -244,18 +244,26 @@ macro_rules! create_config { CliConfigWasSet(self) } - fn fill_from_parsed_config(mut self, parsed: PartialConfig, dir: &Path) -> Config { - $( - if let Some(option_value) = parsed.$i { - let option_stable = self.$i.3; - if $crate::config::config_type::is_stable_option_and_value( - stringify!($i), option_stable, &option_value - ) { - self.$i.1 = true; - self.$i.2 = option_value; + /// Like `Self::fill_from_parsed_config`, but does not do any extra magic besides + /// filling in all config values defined in the given partial config. + #[allow(unreachable_pub)] + pub fn fill_from_partial_config(mut self, partial: PartialConfig) -> Config { + $( + if let Some(option_value) = partial.$i { + let option_stable = self.$i.3; + if $crate::config::config_type::is_stable_option_and_value( + stringify!($i), option_stable, &option_value + ) { + self.$i.1 = true; + self.$i.2 = option_value; + } } - } - )+ + )+ + self + } + + fn fill_from_parsed_config(mut self, parsed: PartialConfig, dir: &Path) -> Config { + self = self.fill_from_partial_config(parsed); self.set_heuristics(); self.set_ignore(dir); self.set_merge_imports(); diff --git a/src/config/editorconfig.rs b/src/config/editorconfig.rs new file mode 100644 index 00000000000..f15cbd581a5 --- /dev/null +++ b/src/config/editorconfig.rs @@ -0,0 +1,102 @@ +//! This module adapts between .editorconfig values and rustfmt’s formatting options. + +use std::path::Path; + +use ec4rs::{ + Properties, properties_of, + property::{ + Charset, EndOfLine, FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, + TrimTrailingWs, + }, +}; + +use crate::{NewlineStyle, config::PartialConfig}; + +/// Reads any applicable .editorconfig values for the given file, +/// and converts it into rustfmt’s config options. +/// Returns None if there is no applicable .editorconfig. +pub fn editorconfig_configuration_for( + file: impl AsRef, +) -> Result, String> { + let mut properties = properties_of(file).map_err(|e| e.to_string())?; + properties.use_fallbacks(); + Ok(Some(properties.try_into()?)) +} + +impl TryFrom for PartialConfig { + type Error = String; + + fn try_from(properties: Properties) -> Result { + let mut config = PartialConfig::default(); + + // 1. Ensure that "unconfigurable" keys have the values we expect, if present. + // Only utf-8 charsets are permitted. + let charset = get_editorconfig_property::(&properties)?; + if let Some(charset) = charset { + if ![Charset::Utf8, Charset::Utf8Bom].contains(&charset) { + return Err(format!("Invalid charset '{charset}'",)); + } + } + // Always true. + let trim_trailing_whitespace = get_editorconfig_property::(&properties)?; + if let Some(TrimTrailingWs::Value(false)) = trim_trailing_whitespace { + return Err(format!("trim_trailing_whitespace must not be false",)); + } + // Always true. + let insert_final_newline = get_editorconfig_property::(&properties)?; + if let Some(FinalNewline::Value(false)) = insert_final_newline { + return Err(format!("insert_final_newline must not be false",)); + } + + // 2. Actually parse configurable keys. + let indent_style = get_editorconfig_property::(&properties)?; + let indent_size = get_editorconfig_property::(&properties)?; + let tab_width = get_editorconfig_property::(&properties)?; + let end_of_line = get_editorconfig_property::(&properties)?; + let max_line_len = get_editorconfig_property::(&properties)?; + + if let Some(indent_style) = indent_style { + config.hard_tabs = Some(indent_style == IndentStyle::Tabs); + } + if let Some(IndentSize::Value(indent_size)) = indent_size { + config.tab_spaces = Some(indent_size); + } + // Note: If the tab width is not set, the specification says to use + // "the tab size set by the editor." + // We interpret this to mean "the default/fallback value", + // and don’t specify tab_spaces in this case. + if let Some(IndentSize::UseTabWidth) = indent_size { + if let Some(TabWidth::Value(indent_size)) = tab_width { + config.tab_spaces = Some(indent_size); + } + } + if let Some(end_of_line) = end_of_line { + config.newline_style = match end_of_line { + EndOfLine::Lf => Some(NewlineStyle::Unix), + EndOfLine::CrLf => Some(NewlineStyle::Windows), + EndOfLine::Cr => return Err("'end_of_line=cr' is not supported".to_owned()), + }; + } + if let Some(MaxLineLen::Value(max_line_len)) = max_line_len { + config.max_width = Some(max_line_len); + } + + Ok(config) + } +} + +/// Improved wrapper around [`ec4rs::Properties::get`]. +fn get_editorconfig_property( + properties: &ec4rs::Properties, +) -> Result, String> { + properties.get::().map_or_else( + |raw| { + if raw.filter_unset().is_unset() { + Ok(None) + } else { + Err(format!("Invalid value {raw}")) + } + }, + |value| Ok(Some(value)), + ) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index a3f9842cd4f..d05bffb4bc4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,6 +8,8 @@ use thiserror::Error; use crate::config::config_type::ConfigType; #[allow(unreachable_pub)] +pub use crate::config::editorconfig::editorconfig_configuration_for; +#[allow(unreachable_pub)] pub use crate::config::file_lines::{FileLines, FileName, Range}; #[allow(unreachable_pub)] pub use crate::config::macro_names::MacroSelector; @@ -20,6 +22,7 @@ pub(crate) mod config_type; #[allow(unreachable_pub)] pub(crate) mod options; +pub(crate) mod editorconfig; pub(crate) mod file_lines; #[allow(unreachable_pub)] pub(crate) mod lists; @@ -457,25 +460,31 @@ impl Config { /// Loads a config by checking the client-supplied options and if appropriate, the /// file system (including searching the file system for overrides). +/// `file_path` is either a directory name, in which case the config is loaded only +/// from the applicable rustfmt.toml. +/// If it is a path to a Rust file, and the options request this, it may additionally +/// load the config from the .editorconfig file. pub fn load_config( - file_path: Option<&Path>, + file_or_directory: Option<&Path>, options: Option, ) -> Result<(Config, Option), Error> { - let (over_ride, edition, style_edition, version) = match options { + let (over_ride, edition, style_edition, version, use_editorconfig) = match options { Some(ref opts) => ( config_path(opts)?, opts.edition(), opts.style_edition(), opts.version(), + opts.use_editorconfig(), ), - None => (None, None, None, None), + None => (None, None, None, None, false), }; + let config_dir = file_or_directory.and_then(|f| if f.is_file() { f.parent() } else { Some(f) }); let result = if let Some(over_ride) = over_ride { Config::from_toml_path(over_ride.as_ref(), edition, style_edition, version) .map(|p| (p, Some(over_ride.to_owned()))) - } else if let Some(file_path) = file_path { - Config::from_resolved_toml_path(file_path, edition, style_edition, version) + } else if let Some(config_dir) = config_dir { + Config::from_resolved_toml_path(config_dir, edition, style_edition, version) } else { Ok(( Config::default_for_possible_style_edition(style_edition, edition, version), @@ -483,7 +492,22 @@ pub fn load_config( )) }; + let editorconfig = if use_editorconfig && file_or_directory.is_some_and(|f| f.is_file()) { + if let Some(file) = file_or_directory { + editorconfig_configuration_for(&file) + .map_err(|e| Error::other(format!("Invalid .editorconfig: {e}")))? + } else { + unreachable!() + } + } else { + None + }; + result.map(|(mut c, p)| { + if let Some(editorconfig) = editorconfig { + c = c.fill_from_partial_config(editorconfig); + } + if let Some(options) = options { options.apply_to(&mut c); } diff --git a/src/config/options.rs b/src/config/options.rs index db48d99604a..0a3800c4bc1 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -465,6 +465,7 @@ pub trait CliOptions { /// It is ok if the returned path doesn't exist or is not canonicalized /// (i.e. the callers are expected to handle such cases). fn config_path(&self) -> Option<&Path>; + fn use_editorconfig(&self) -> bool; fn edition(&self) -> Option; fn style_edition(&self) -> Option; fn version(&self) -> Option; diff --git a/src/git-rustfmt/main.rs b/src/git-rustfmt/main.rs index 64458420f59..c18baa7c5ec 100644 --- a/src/git-rustfmt/main.rs +++ b/src/git-rustfmt/main.rs @@ -98,6 +98,9 @@ impl CliOptions for NullOptions { fn version(&self) -> Option { unreachable!(); } + fn use_editorconfig(&self) -> bool { + unreachable!() + } } fn uncommitted_files() -> Vec { diff --git a/src/lib.rs b/src/lib.rs index 285fa26d00a..488526749ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ use crate::utils::indent_next_line; pub use crate::config::{ CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName, NewlineStyle, Range, - StyleEdition, Verbosity, Version, load_config, + StyleEdition, Verbosity, Version, editorconfig_configuration_for, load_config, }; pub use crate::format_report_formatter::{FormatReportFormatter, FormatReportFormatterBuilder}; diff --git a/tests/editorconfig/errors/cr/.editorconfig b/tests/editorconfig/errors/cr/.editorconfig new file mode 100644 index 00000000000..0d5b7bebe44 --- /dev/null +++ b/tests/editorconfig/errors/cr/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: 'end_of_line=cr' is not supported + +root = true + +[*.rs] +end_of_line = cr diff --git a/tests/editorconfig/errors/cr/main.rs b/tests/editorconfig/errors/cr/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/cr/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-charset/.editorconfig b/tests/editorconfig/errors/invalid-charset/.editorconfig new file mode 100644 index 00000000000..cf1690d651f --- /dev/null +++ b/tests/editorconfig/errors/invalid-charset/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value javascript-or-something-idk + +root = true + +[*.rs] +charset = javascript-or-something-idk diff --git a/tests/editorconfig/errors/invalid-charset/main.rs b/tests/editorconfig/errors/invalid-charset/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-charset/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-editorconfig/.editorconfig b/tests/editorconfig/errors/invalid-editorconfig/.editorconfig new file mode 100644 index 00000000000..b1c14ec3e9e --- /dev/null +++ b/tests/editorconfig/errors/invalid-editorconfig/.editorconfig @@ -0,0 +1,5 @@ +# rustfmt-expect: Invalid .editorconfig + +[{asdjk...iiid}] +charsetutf-8 +?????%% diff --git a/tests/editorconfig/errors/invalid-editorconfig/main.rs b/tests/editorconfig/errors/invalid-editorconfig/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-editorconfig/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-eol/.editorconfig b/tests/editorconfig/errors/invalid-eol/.editorconfig new file mode 100644 index 00000000000..85a73ca2745 --- /dev/null +++ b/tests/editorconfig/errors/invalid-eol/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value invalid + +root = true + +[*.rs] +end_of_line = invalid diff --git a/tests/editorconfig/errors/invalid-eol/main.rs b/tests/editorconfig/errors/invalid-eol/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-eol/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-indent-size/.editorconfig b/tests/editorconfig/errors/invalid-indent-size/.editorconfig new file mode 100644 index 00000000000..653da65ad09 --- /dev/null +++ b/tests/editorconfig/errors/invalid-indent-size/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value garbage + +root = true + +[*.rs] +indent_size = garbage diff --git a/tests/editorconfig/errors/invalid-indent-size/main.rs b/tests/editorconfig/errors/invalid-indent-size/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-indent-size/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-indent-style/.editorconfig b/tests/editorconfig/errors/invalid-indent-style/.editorconfig new file mode 100644 index 00000000000..72ae818c9fc --- /dev/null +++ b/tests/editorconfig/errors/invalid-indent-style/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value owo + +root = true + +[*.rs] +indent_style = owo diff --git a/tests/editorconfig/errors/invalid-indent-style/main.rs b/tests/editorconfig/errors/invalid-indent-style/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-indent-style/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-insert-final-newline/.editorconfig b/tests/editorconfig/errors/invalid-insert-final-newline/.editorconfig new file mode 100644 index 00000000000..a006ac8a758 --- /dev/null +++ b/tests/editorconfig/errors/invalid-insert-final-newline/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value *biscuits* + +root = true + +[*.rs] +insert_final_newline = *biscuits* diff --git a/tests/editorconfig/errors/invalid-insert-final-newline/main.rs b/tests/editorconfig/errors/invalid-insert-final-newline/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-insert-final-newline/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-max-line-len/.editorconfig b/tests/editorconfig/errors/invalid-max-line-len/.editorconfig new file mode 100644 index 00000000000..df6772e9cee --- /dev/null +++ b/tests/editorconfig/errors/invalid-max-line-len/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value uwu + +root = true + +[*.rs] +max_line_length = uwu diff --git a/tests/editorconfig/errors/invalid-max-line-len/main.rs b/tests/editorconfig/errors/invalid-max-line-len/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-max-line-len/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-tab-width/.editorconfig b/tests/editorconfig/errors/invalid-tab-width/.editorconfig new file mode 100644 index 00000000000..0400860da34 --- /dev/null +++ b/tests/editorconfig/errors/invalid-tab-width/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value mrrrp + +root = true + +[*.rs] +tab_width = mrrrp diff --git a/tests/editorconfig/errors/invalid-tab-width/main.rs b/tests/editorconfig/errors/invalid-tab-width/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-tab-width/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/invalid-trim-trailing-ws/.editorconfig b/tests/editorconfig/errors/invalid-trim-trailing-ws/.editorconfig new file mode 100644 index 00000000000..2b178944ac1 --- /dev/null +++ b/tests/editorconfig/errors/invalid-trim-trailing-ws/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid value *paws* + +root = true + +[*.rs] +trim_trailing_whitespace = *paws* diff --git a/tests/editorconfig/errors/invalid-trim-trailing-ws/main.rs b/tests/editorconfig/errors/invalid-trim-trailing-ws/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/invalid-trim-trailing-ws/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/unsupported-charset/.editorconfig b/tests/editorconfig/errors/unsupported-charset/.editorconfig new file mode 100644 index 00000000000..93cd18fa7f4 --- /dev/null +++ b/tests/editorconfig/errors/unsupported-charset/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: Invalid charset 'latin1' + +root = true + +[*.rs] +charset = latin1 diff --git a/tests/editorconfig/errors/unsupported-charset/main.rs b/tests/editorconfig/errors/unsupported-charset/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/unsupported-charset/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/unsupported-insert-final-newline/.editorconfig b/tests/editorconfig/errors/unsupported-insert-final-newline/.editorconfig new file mode 100644 index 00000000000..32a5aca86e5 --- /dev/null +++ b/tests/editorconfig/errors/unsupported-insert-final-newline/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: insert_final_newline must not be false + +root = true + +[*.rs] +insert_final_newline = false diff --git a/tests/editorconfig/errors/unsupported-insert-final-newline/main.rs b/tests/editorconfig/errors/unsupported-insert-final-newline/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/unsupported-insert-final-newline/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/errors/unsupported-trim-trailing-ws/.editorconfig b/tests/editorconfig/errors/unsupported-trim-trailing-ws/.editorconfig new file mode 100644 index 00000000000..420eb6908b3 --- /dev/null +++ b/tests/editorconfig/errors/unsupported-trim-trailing-ws/.editorconfig @@ -0,0 +1,6 @@ +# rustfmt-expect: Invalid .editorconfig: trim_trailing_whitespace must not be false + +root = true + +[*.rs] +trim_trailing_whitespace = false diff --git a/tests/editorconfig/errors/unsupported-trim-trailing-ws/main.rs b/tests/editorconfig/errors/unsupported-trim-trailing-ws/main.rs new file mode 100644 index 00000000000..2a778e4ea1a --- /dev/null +++ b/tests/editorconfig/errors/unsupported-trim-trailing-ws/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} diff --git a/tests/editorconfig/main.rs b/tests/editorconfig/main.rs new file mode 100644 index 00000000000..d2adc343224 --- /dev/null +++ b/tests/editorconfig/main.rs @@ -0,0 +1,112 @@ +#![allow(unused)] // on stable + +use rustfmt_config_proc_macro::nightly_only_test; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{env, fs}; + +use tempfile::TempDir; + +fn rustfmt_binary() -> PathBuf { + let mut me = env::current_exe().expect("failed to get current executable"); + // Chop of the test name. + me.pop(); + + // Handle Cargo's old and new filesystem layouts + // * v1: `target//deps/test-bin-[HASH][EXE]` + // * v2: `target//build//[HASH]/out/test-bin-[HASH][EXE]` + if me.ends_with("deps") { + // Chop off `deps`. + me.pop(); + } else if me.ends_with("out") { + // Chop off `out`. + me.pop(); + // Chop off ``. + me.pop(); + // Chop off ``. + me.pop(); + // Chop off `build`. + me.pop(); + } + + me.push("rustfmt"); + assert!( + me.is_file() || me.with_extension("exe").is_file(), + "{}", + "no rustfmt bin, try running `cargo build --locked` or `cargo build --locked --release` \ + before testing" + ); + me +} + +/// Run rustfmt and return its output. +fn rustfmt(args: &[&str]) -> (String, String) { + let rustfmt = rustfmt_binary(); + let cmd = rustfmt; + + match Command::new(&cmd).args(args).output() { + Ok(output) => ( + String::from_utf8(output.stdout).expect("utf-8"), + String::from_utf8(output.stderr).expect("utf-8"), + ), + Err(e) => panic!("failed to run `{cmd:?} {args:?}`: {e}"), + } +} + +#[nightly_only_test] +#[test] +fn integration() { + let source_base_path = Path::new("tests/editorconfig/source"); + let target_base_path = Path::new("tests/editorconfig/target"); + for test_directory in source_base_path.read_dir().unwrap() { + let source_directory = test_directory.unwrap().path(); + let target_directory = target_base_path.join(source_directory.file_name().unwrap()); + let temp_source_directory = TempDir::new().unwrap(); + let output_directory = temp_source_directory.path(); + for required_file in ["main.rs", ".editorconfig"] { + fs::copy( + source_directory.join(required_file), + output_directory.join(required_file), + ) + .unwrap(); + } + let _ = rustfmt(&[ + "--unstable-features", + "--use-editorconfig", + output_directory.join("main.rs").to_string_lossy().as_ref(), + ]); + + let reference_source = fs::read_to_string(target_directory.join("main.rs")).unwrap(); + let result_source = fs::read_to_string(output_directory.join("main.rs")).unwrap(); + assert_eq!(result_source, reference_source); + drop(temp_source_directory); + } +} + +#[nightly_only_test] +#[test] +fn integration_errors() { + let base_path = Path::new("tests/editorconfig/errors"); + for test_directory in base_path.read_dir().unwrap() { + let test_directory = test_directory.unwrap().path(); + let (_, stderr) = rustfmt(&[ + "--unstable-features", + "--use-editorconfig", + "--check", + test_directory.join("main.rs").to_string_lossy().as_ref(), + ]); + println!("{stderr}"); + let editorconfig = fs::read_to_string(test_directory.join(".editorconfig")).unwrap(); + let expected_error = editorconfig + .lines() + .next() + .unwrap() + .strip_prefix("# rustfmt-expect: "); + if let Some(expected_error) = expected_error { + println!("Checking contents of '{stderr}' for '{expected_error}'…"); + assert!(stderr.contains(expected_error)); + } else { + assert!(stderr.is_empty()); + } + } +} diff --git a/tests/editorconfig/source/basic/.editorconfig b/tests/editorconfig/source/basic/.editorconfig new file mode 100644 index 00000000000..b51566ce847 --- /dev/null +++ b/tests/editorconfig/source/basic/.editorconfig @@ -0,0 +1,8 @@ +root = true + +# Default Rust settings +[*.rs] +indent_size = 4 +indent_style = space +end_of_line = lf +charset = utf-8 diff --git a/tests/editorconfig/source/basic/main.rs b/tests/editorconfig/source/basic/main.rs new file mode 100644 index 00000000000..5f0a31a6d32 --- /dev/null +++ b/tests/editorconfig/source/basic/main.rs @@ -0,0 +1,10 @@ +fn main () { /* badly formatted file */} + + const WHAT_IS_GOING_ON : u32 = + + + + 3 + + + ; \ No newline at end of file diff --git a/tests/editorconfig/source/eol-crlf/.editorconfig b/tests/editorconfig/source/eol-crlf/.editorconfig new file mode 100644 index 00000000000..6000b8ea418 --- /dev/null +++ b/tests/editorconfig/source/eol-crlf/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +# Make Bill Gates happy UwU +end_of_line = crlf +charset = utf-8-bom diff --git a/tests/editorconfig/source/eol-crlf/main.rs b/tests/editorconfig/source/eol-crlf/main.rs new file mode 100644 index 00000000000..c98f638a3b6 --- /dev/null +++ b/tests/editorconfig/source/eol-crlf/main.rs @@ -0,0 +1,10 @@ +// Note: The source version of this file has `\n` (LF) line endings, the target has `\r\n` (CRLF). + +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/source/indent-size/.editorconfig b/tests/editorconfig/source/indent-size/.editorconfig new file mode 100644 index 00000000000..b8c0648b313 --- /dev/null +++ b/tests/editorconfig/source/indent-size/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*.rs] +indent_size = 7 diff --git a/tests/editorconfig/source/indent-size/main.rs b/tests/editorconfig/source/indent-size/main.rs new file mode 100644 index 00000000000..c0dc3c762a8 --- /dev/null +++ b/tests/editorconfig/source/indent-size/main.rs @@ -0,0 +1,8 @@ +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/source/max-line-len/.editorconfig b/tests/editorconfig/source/max-line-len/.editorconfig new file mode 100644 index 00000000000..ba3391264b2 --- /dev/null +++ b/tests/editorconfig/source/max-line-len/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +max_line_length = 40 diff --git a/tests/editorconfig/source/max-line-len/main.rs b/tests/editorconfig/source/max-line-len/main.rs new file mode 100644 index 00000000000..ce2fcb9825d --- /dev/null +++ b/tests/editorconfig/source/max-line-len/main.rs @@ -0,0 +1,4 @@ +fn main() { + // Should be wrapped due to max_line_len. + let value = 5940930490 + 2398329284308320 - 83498439734589374 + 29304920934029034 * 20934829384928340 / 234820984309234; +} diff --git a/tests/editorconfig/source/tab-indent/.editorconfig b/tests/editorconfig/source/tab-indent/.editorconfig new file mode 100644 index 00000000000..b39b7e54800 --- /dev/null +++ b/tests/editorconfig/source/tab-indent/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +indent_style = tab diff --git a/tests/editorconfig/source/tab-indent/main.rs b/tests/editorconfig/source/tab-indent/main.rs new file mode 100644 index 00000000000..c0dc3c762a8 --- /dev/null +++ b/tests/editorconfig/source/tab-indent/main.rs @@ -0,0 +1,8 @@ +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/source/tab-size/.editorconfig b/tests/editorconfig/source/tab-size/.editorconfig new file mode 100644 index 00000000000..b69e994d0bb --- /dev/null +++ b/tests/editorconfig/source/tab-size/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +indent_style = space +indent_size = tab +tab_width = 2 diff --git a/tests/editorconfig/source/tab-size/main.rs b/tests/editorconfig/source/tab-size/main.rs new file mode 100644 index 00000000000..c0dc3c762a8 --- /dev/null +++ b/tests/editorconfig/source/tab-size/main.rs @@ -0,0 +1,8 @@ +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/target/basic/main.rs b/tests/editorconfig/target/basic/main.rs new file mode 100644 index 00000000000..ea7fc5ba503 --- /dev/null +++ b/tests/editorconfig/target/basic/main.rs @@ -0,0 +1,4 @@ +fn main() { /* badly formatted file */ +} + +const WHAT_IS_GOING_ON: u32 = 3; diff --git a/tests/editorconfig/target/eol-crlf/main.rs b/tests/editorconfig/target/eol-crlf/main.rs new file mode 100644 index 00000000000..d6cae17f033 --- /dev/null +++ b/tests/editorconfig/target/eol-crlf/main.rs @@ -0,0 +1,10 @@ +// Note: The source version of this file has `\n` (LF) line endings, the target has `\r\n` (CRLF). + +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/target/indent-size/main.rs b/tests/editorconfig/target/indent-size/main.rs new file mode 100644 index 00000000000..3eac300d1d2 --- /dev/null +++ b/tests/editorconfig/target/indent-size/main.rs @@ -0,0 +1,8 @@ +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/target/max-line-len/main.rs b/tests/editorconfig/target/max-line-len/main.rs new file mode 100644 index 00000000000..d95a230c1e9 --- /dev/null +++ b/tests/editorconfig/target/max-line-len/main.rs @@ -0,0 +1,9 @@ +fn main() { + // Should be wrapped due to max_line_len. + let value = 5940930490 + + 2398329284308320 + - 83498439734589374 + + 29304920934029034 + * 20934829384928340 + / 234820984309234; +} diff --git a/tests/editorconfig/target/tab-indent/main.rs b/tests/editorconfig/target/tab-indent/main.rs new file mode 100644 index 00000000000..82319edb8b8 --- /dev/null +++ b/tests/editorconfig/target/tab-indent/main.rs @@ -0,0 +1,8 @@ +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/editorconfig/target/tab-size/main.rs b/tests/editorconfig/target/tab-size/main.rs new file mode 100644 index 00000000000..708f98048e0 --- /dev/null +++ b/tests/editorconfig/target/tab-size/main.rs @@ -0,0 +1,8 @@ +fn main() { + let value = 5; + if value == 0 { + println!("meow"); + } else { + println!("owo"); + } +} diff --git a/tests/rustfmt/main.rs b/tests/rustfmt/main.rs index ee972d8a6af..db5720abc3b 100644 --- a/tests/rustfmt/main.rs +++ b/tests/rustfmt/main.rs @@ -217,6 +217,8 @@ fn rustfmt_nightly_usage_text() { whitespaces (unstable). --skip-children Don't reformat child modules (unstable). + --use-editorconfig + Respect .editorconfig settings for Rust source files. -v, --verbose Print verbose output -q, --quiet Print less output -V, --version Show version information