From 15b5287562fd8a73276c7839b6332dcc2a8455d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Sat, 19 Sep 2026 23:35:00 +0200 Subject: [PATCH 1/2] Support .editorconfig in rustfmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new unstable option `--use-editorconfig` allows rustfmt to respect .editorconfig, which configures several basic (all stable) options of rustfmt. For the user, .editorconfig obviates the need for rustfmt.toml in most basic use cases, and allows IDEs (most of which respect .editorconfig) and LSPs (most of which use rustfmt to format code interactively) to better work together for Rust code formatting. In terms of implementation, this code mainly affects `load_config`, the central entry point for configuration discovery. This function’s argument previously always received a folder from which to search for rustfmt.toml (despite confusingly being named "file_path"). Now, this argument may alternatively receive a file path directly, as most users now do, wherever practical. In this case, and if the options allow this behavior, .editorconfig is loaded and respected, overall almost identically to rustfmt.toml. This adds a dependency to ec4rs, the best .editorconfig implementation in Rust, which passes the editorconfig specification test suite and overall behaves exactly as expected. These changes have approximately 95% test coverage, including almost every practically possible error case and all possible .editorconfig configurations and their effects. This option does not affect rustfmt’s stable behavior in any way. The current rustfmt maintainer stance is that .editorconfig should never be respected by default, so this will likely remain the case after stabilization. The commits after this one will adjust documentation and add pass-through support for cargo-fmt. --- .gitattributes | 1 + Cargo.lock | 7 ++ Cargo.toml | 1 + src/bin/main.rs | 21 +++- src/config/config_type.rs | 32 +++-- src/config/editorconfig.rs | 102 ++++++++++++++++ src/config/mod.rs | 34 +++++- src/config/options.rs | 1 + src/git-rustfmt/main.rs | 3 + src/lib.rs | 2 +- tests/editorconfig/errors/cr/.editorconfig | 6 + tests/editorconfig/errors/cr/main.rs | 3 + .../errors/invalid-charset/.editorconfig | 6 + .../errors/invalid-charset/main.rs | 3 + .../errors/invalid-editorconfig/.editorconfig | 5 + .../errors/invalid-editorconfig/main.rs | 3 + .../errors/invalid-eol/.editorconfig | 6 + tests/editorconfig/errors/invalid-eol/main.rs | 3 + .../errors/invalid-indent-size/.editorconfig | 6 + .../errors/invalid-indent-size/main.rs | 3 + .../errors/invalid-indent-style/.editorconfig | 6 + .../errors/invalid-indent-style/main.rs | 3 + .../.editorconfig | 6 + .../invalid-insert-final-newline/main.rs | 3 + .../errors/invalid-max-line-len/.editorconfig | 6 + .../errors/invalid-max-line-len/main.rs | 3 + .../errors/invalid-tab-width/.editorconfig | 6 + .../errors/invalid-tab-width/main.rs | 3 + .../invalid-trim-trailing-ws/.editorconfig | 6 + .../errors/invalid-trim-trailing-ws/main.rs | 3 + .../errors/unsupported-charset/.editorconfig | 6 + .../errors/unsupported-charset/main.rs | 3 + .../.editorconfig | 6 + .../unsupported-insert-final-newline/main.rs | 3 + .../.editorconfig | 6 + .../unsupported-trim-trailing-ws/main.rs | 3 + tests/editorconfig/main.rs | 112 ++++++++++++++++++ tests/editorconfig/source/basic/.editorconfig | 8 ++ tests/editorconfig/source/basic/main.rs | 10 ++ .../source/eol-crlf/.editorconfig | 6 + tests/editorconfig/source/eol-crlf/main.rs | 10 ++ .../source/indent-size/.editorconfig | 4 + tests/editorconfig/source/indent-size/main.rs | 8 ++ .../source/max-line-len/.editorconfig | 4 + .../editorconfig/source/max-line-len/main.rs | 4 + .../source/tab-indent/.editorconfig | 4 + tests/editorconfig/source/tab-indent/main.rs | 8 ++ .../source/tab-size/.editorconfig | 6 + tests/editorconfig/source/tab-size/main.rs | 8 ++ tests/editorconfig/target/basic/main.rs | 4 + tests/editorconfig/target/eol-crlf/main.rs | 10 ++ tests/editorconfig/target/indent-size/main.rs | 8 ++ .../editorconfig/target/max-line-len/main.rs | 9 ++ tests/editorconfig/target/tab-indent/main.rs | 8 ++ tests/editorconfig/target/tab-size/main.rs | 8 ++ tests/rustfmt/main.rs | 2 + 56 files changed, 540 insertions(+), 21 deletions(-) create mode 100644 src/config/editorconfig.rs create mode 100644 tests/editorconfig/errors/cr/.editorconfig create mode 100644 tests/editorconfig/errors/cr/main.rs create mode 100644 tests/editorconfig/errors/invalid-charset/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-charset/main.rs create mode 100644 tests/editorconfig/errors/invalid-editorconfig/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-editorconfig/main.rs create mode 100644 tests/editorconfig/errors/invalid-eol/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-eol/main.rs create mode 100644 tests/editorconfig/errors/invalid-indent-size/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-indent-size/main.rs create mode 100644 tests/editorconfig/errors/invalid-indent-style/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-indent-style/main.rs create mode 100644 tests/editorconfig/errors/invalid-insert-final-newline/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-insert-final-newline/main.rs create mode 100644 tests/editorconfig/errors/invalid-max-line-len/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-max-line-len/main.rs create mode 100644 tests/editorconfig/errors/invalid-tab-width/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-tab-width/main.rs create mode 100644 tests/editorconfig/errors/invalid-trim-trailing-ws/.editorconfig create mode 100644 tests/editorconfig/errors/invalid-trim-trailing-ws/main.rs create mode 100644 tests/editorconfig/errors/unsupported-charset/.editorconfig create mode 100644 tests/editorconfig/errors/unsupported-charset/main.rs create mode 100644 tests/editorconfig/errors/unsupported-insert-final-newline/.editorconfig create mode 100644 tests/editorconfig/errors/unsupported-insert-final-newline/main.rs create mode 100644 tests/editorconfig/errors/unsupported-trim-trailing-ws/.editorconfig create mode 100644 tests/editorconfig/errors/unsupported-trim-trailing-ws/main.rs create mode 100644 tests/editorconfig/main.rs create mode 100644 tests/editorconfig/source/basic/.editorconfig create mode 100644 tests/editorconfig/source/basic/main.rs create mode 100644 tests/editorconfig/source/eol-crlf/.editorconfig create mode 100644 tests/editorconfig/source/eol-crlf/main.rs create mode 100644 tests/editorconfig/source/indent-size/.editorconfig create mode 100644 tests/editorconfig/source/indent-size/main.rs create mode 100644 tests/editorconfig/source/max-line-len/.editorconfig create mode 100644 tests/editorconfig/source/max-line-len/main.rs create mode 100644 tests/editorconfig/source/tab-indent/.editorconfig create mode 100644 tests/editorconfig/source/tab-indent/main.rs create mode 100644 tests/editorconfig/source/tab-size/.editorconfig create mode 100644 tests/editorconfig/source/tab-size/main.rs create mode 100644 tests/editorconfig/target/basic/main.rs create mode 100644 tests/editorconfig/target/eol-crlf/main.rs create mode 100644 tests/editorconfig/target/indent-size/main.rs create mode 100644 tests/editorconfig/target/max-line-len/main.rs create mode 100644 tests/editorconfig/target/tab-indent/main.rs create mode 100644 tests/editorconfig/target/tab-size/main.rs 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/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 From 0dcb25e1423c8e561c0ad9979989eb982c1805be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Mon, 21 Sep 2026 15:34:22 +0200 Subject: [PATCH 2/2] Add documentation for .editorconfig support --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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