Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 18 additions & 3 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -255,7 +260,7 @@ fn execute(opts: &Options) -> Result<i32> {
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())?;

Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -531,6 +535,7 @@ struct GetOptsOptions {
quiet: bool,
verbose: bool,
config_path: Option<PathBuf>,
use_editorconfig: bool,
inline_config: HashMap<String, String>,
emit_mode: Option<EmitMode>,
backup: bool,
Expand Down Expand Up @@ -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") {
Expand All @@ -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!(
Expand Down Expand Up @@ -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<Edition> {
Expand Down
32 changes: 20 additions & 12 deletions src/config/config_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>),+
Expand Down Expand Up @@ -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();
Expand Down
102 changes: 102 additions & 0 deletions src/config/editorconfig.rs
Original file line number Diff line number Diff line change
@@ -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<Path>,
) -> Result<Option<PartialConfig>, String> {
let mut properties = properties_of(file).map_err(|e| e.to_string())?;
properties.use_fallbacks();
Ok(Some(properties.try_into()?))
}

impl TryFrom<Properties> for PartialConfig {
type Error = String;

fn try_from(properties: Properties) -> Result<Self, Self::Error> {
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::<Charset>(&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::<TrimTrailingWs>(&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::<FinalNewline>(&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::<IndentStyle>(&properties)?;
let indent_size = get_editorconfig_property::<IndentSize>(&properties)?;
let tab_width = get_editorconfig_property::<TabWidth>(&properties)?;
let end_of_line = get_editorconfig_property::<EndOfLine>(&properties)?;
let max_line_len = get_editorconfig_property::<MaxLineLen>(&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<T: ec4rs::PropertyKey + ec4rs::PropertyValue>(
properties: &ec4rs::Properties,
) -> Result<Option<T>, String> {
properties.get::<T>().map_or_else(
|raw| {
if raw.filter_unset().is_unset() {
Ok(None)
} else {
Err(format!("Invalid value {raw}"))
}
},
|value| Ok(Some(value)),
)
}
34 changes: 29 additions & 5 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -457,33 +460,54 @@ 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<O: CliOptions>(
file_path: Option<&Path>,
file_or_directory: Option<&Path>,
options: Option<O>,
) -> Result<(Config, Option<PathBuf>), 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),
None,
))
};

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);
}
Expand Down
1 change: 1 addition & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Edition>;
fn style_edition(&self) -> Option<StyleEdition>;
fn version(&self) -> Option<Version>;
Expand Down
3 changes: 3 additions & 0 deletions src/git-rustfmt/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ impl CliOptions for NullOptions {
fn version(&self) -> Option<Version> {
unreachable!();
}
fn use_editorconfig(&self) -> bool {
unreachable!()
}
}

fn uncommitted_files() -> Vec<String> {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading
Loading