Skip to content

Commit 9cf09c7

Browse files
committed
Merge configs from parent directories
Currently, `rustfmt` stops building its configuration after one configuration file is found. However, users may expect that `rustfmt` includes configuration options inhereted by configuration files in parent directories of the directory `rustfmt` is run in. The motivation for this commit is for a use case involving files that should be ignored by `rustfmt`. Consider the directory structure ``` a |-- rustfmt.toml |-- b |-- rustfmt.toml |-- main.rs ``` Suppose `a/rustfmt.toml` has the option `ignore = ["b/main.rs"]`. A user may expect that running `rusfmt` in either directory `a/` or `b/` will ignore `main.rs`. Today, though, if `rustfmt` is run in `b/`, `main.rs` will be formatted because only `b/rustfmt.toml` will be used for configuration. Although motivated by the use case for ignored files, this change sets up `rustfmt` to incrementally populate all configuration options from parent config files. The priority of population is closest config file to most transient config file. To avoid churn of the existing implementation for ignoring files, populating the ignored files config option with multiple config files is done by computing a join. Given config files "outer" and "inner", where "inner" is of higher priority (nested in the directory of) "outer", we merge their `ignore` configurations by finding the ignore files specified in "outer" that are present in the "inner" directory, and appending this to the files ignored by "inner". This means that printing an `ignore` configuration may not be entirely accurate, as it could be missing files that are not in the current directory specified by transient configurations. These files are out of the scope the `rustfmt` instance's operation, though, so for practical purposes I don't think this matters. Closes #3881
1 parent e44a71c commit 9cf09c7

File tree

4 files changed

+233
-62
lines changed

4 files changed

+233
-62
lines changed

rustfmt-core/rustfmt-bin/src/bin/main.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ fn format_string(input: String, opt: Opt) -> Result<i32> {
425425

426426
enum FileConfig {
427427
Default,
428-
Local(Config, Option<PathBuf>),
428+
Local(Config, Option<Vec<PathBuf>>),
429429
}
430430

431431
struct FileConfigPair<'a> {
@@ -457,9 +457,9 @@ impl<'a> Iterator for FileConfigPairIter<'a> {
457457
let config = if self.has_config_from_commandline {
458458
FileConfig::Default
459459
} else {
460-
let (local_config, config_path) =
460+
let (local_config, config_paths) =
461461
load_config(Some(file.parent()?), Some(self.opt)).ok()?;
462-
FileConfig::Local(local_config, config_path)
462+
FileConfig::Local(local_config, config_paths)
463463
};
464464

465465
Some(FileConfigPair { file, config })
@@ -483,11 +483,19 @@ fn format(opt: Opt) -> Result<i32> {
483483
return Err(format_err!("Error: `{}` is a directory", dir.display()));
484484
}
485485

486-
let (default_config, config_path) = load_config(None, Some(&opt))?;
486+
let (default_config, config_paths) = load_config(None, Some(&opt))?;
487487

488488
if opt.verbose {
489-
if let Some(path) = config_path.as_ref() {
490-
println!("Using rustfmt config file {}", path.display());
489+
if let Some(paths) = config_paths.as_ref() {
490+
println!(
491+
"Using rustfmt config files {} for {}",
492+
paths
493+
.into_iter()
494+
.map(|p| p.display().to_string())
495+
.collect::<Vec<_>>()
496+
.join(","),
497+
file.display()
498+
);
491499
}
492500
}
493501

@@ -496,7 +504,7 @@ fn format(opt: Opt) -> Result<i32> {
496504
verbosity: opt.verbosity(),
497505
};
498506

499-
let inputs = FileConfigPairIter::new(&opt, config_path.is_some()).collect::<Vec<_>>();
507+
let inputs = FileConfigPairIter::new(&opt, config_paths.is_some()).collect::<Vec<_>>();
500508
let format_report = format_inputs(
501509
inputs.iter().map(|p| {
502510
(

rustfmt-core/rustfmt-lib/src/config.rs

Lines changed: 136 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,43 @@ impl PartialConfig {
170170

171171
::toml::to_string(&cloned).map_err(ToTomlError)
172172
}
173+
174+
pub fn from_toml_path(file_path: &Path) -> Result<PartialConfig, Error> {
175+
let mut file = File::open(&file_path)?;
176+
let mut toml = String::new();
177+
file.read_to_string(&mut toml)?;
178+
PartialConfig::from_toml(&toml).map_err(|err| Error::new(ErrorKind::InvalidData, err))
179+
}
180+
181+
fn from_toml(toml: &str) -> Result<PartialConfig, String> {
182+
let parsed: ::toml::Value = toml
183+
.parse()
184+
.map_err(|e| format!("Could not parse TOML: {}", e))?;
185+
let mut err = String::new();
186+
let table = parsed
187+
.as_table()
188+
.ok_or_else(|| String::from("Parsed config was not table"))?;
189+
for key in table.keys() {
190+
if !Config::is_valid_name(key) {
191+
let msg = &format!("Warning: Unknown configuration option `{}`\n", key);
192+
err.push_str(msg)
193+
}
194+
}
195+
match parsed.try_into() {
196+
Ok(parsed_config) => {
197+
if !err.is_empty() {
198+
eprint!("{}", err);
199+
}
200+
Ok(parsed_config)
201+
}
202+
Err(e) => {
203+
err.push_str("Error: Decoding config file failed:\n");
204+
err.push_str(format!("{}\n", e).as_str());
205+
err.push_str("Please check your config file.");
206+
Err(err)
207+
}
208+
}
209+
}
173210
}
174211

175212
impl Config {
@@ -197,11 +234,8 @@ impl Config {
197234
/// Returns a `Config` if the config could be read and parsed from
198235
/// the file, otherwise errors.
199236
pub fn from_toml_path(file_path: &Path) -> Result<Config, Error> {
200-
let mut file = File::open(&file_path)?;
201-
let mut toml = String::new();
202-
file.read_to_string(&mut toml)?;
203-
Config::from_toml(&toml, file_path.parent().unwrap())
204-
.map_err(|err| Error::new(ErrorKind::InvalidData, err))
237+
let partial_config = PartialConfig::from_toml_path(file_path)?;
238+
Ok(Config::default().fill_from_parsed_config(partial_config, file_path.parent().unwrap()))
205239
}
206240

207241
/// Resolves the config for input in `dir`.
@@ -213,85 +247,74 @@ impl Config {
213247
///
214248
/// Returns the `Config` to use, and the path of the project file if there was
215249
/// one.
216-
pub fn from_resolved_toml_path(dir: &Path) -> Result<(Config, Option<PathBuf>), Error> {
250+
pub fn from_resolved_toml_path(dir: &Path) -> Result<(Config, Option<Vec<PathBuf>>), Error> {
217251
/// Try to find a project file in the given directory and its parents.
218252
/// Returns the path of a the nearest project file if one exists,
219253
/// or `None` if no project file was found.
220-
fn resolve_project_file(dir: &Path) -> Result<Option<PathBuf>, Error> {
254+
fn resolve_project_files(dir: &Path) -> Result<Option<Vec<PathBuf>>, Error> {
221255
let mut current = if dir.is_relative() {
222256
env::current_dir()?.join(dir)
223257
} else {
224258
dir.to_path_buf()
225259
};
226260

227261
current = dunce::canonicalize(current)?;
262+
let mut paths = Vec::new();
228263

229264
loop {
230-
match get_toml_path(&current) {
231-
Ok(Some(path)) => return Ok(Some(path)),
232-
Err(e) => return Err(e),
233-
_ => (),
234-
}
265+
let current_toml_path = get_toml_path(&current)?;
266+
paths.push(current_toml_path);
235267

236268
// If the current directory has no parent, we're done searching.
237269
if !current.pop() {
238270
break;
239271
}
240272
}
241273

274+
if !paths.is_empty() {
275+
return Ok(paths.into_iter().filter(|p| p.is_some()).collect());
276+
}
277+
242278
// If nothing was found, check in the home directory.
243279
if let Some(home_dir) = dirs::home_dir() {
244280
if let Some(path) = get_toml_path(&home_dir)? {
245-
return Ok(Some(path));
281+
return Ok(Some(vec![path]));
246282
}
247283
}
248284

249285
// If none was found ther either, check in the user's configuration directory.
250286
if let Some(mut config_dir) = dirs::config_dir() {
251287
config_dir.push("rustfmt");
252288
if let Some(path) = get_toml_path(&config_dir)? {
253-
return Ok(Some(path));
289+
return Ok(Some(vec![path]));
254290
}
255291
}
256292

257293
Ok(None)
258294
}
259295

260-
match resolve_project_file(dir)? {
296+
let files = resolve_project_files(dir);
297+
298+
match files? {
261299
None => Ok((Config::default(), None)),
262-
Some(path) => Config::from_toml_path(&path).map(|config| (config, Some(path))),
300+
Some(paths) => {
301+
let mut config = Config::default();
302+
let mut used_paths = Vec::with_capacity(paths.len());
303+
for path in paths.into_iter().rev() {
304+
let partial_config = PartialConfig::from_toml_path(&path)?;
305+
config = config.fill_from_parsed_config(partial_config, &path);
306+
used_paths.push(path);
307+
}
308+
309+
Ok((config, Some(used_paths)))
310+
}
263311
}
264312
}
265313

266314
pub fn from_toml(toml: &str, dir: &Path) -> Result<Config, String> {
267-
let parsed: ::toml::Value = toml
268-
.parse()
269-
.map_err(|e| format!("Could not parse TOML: {}", e))?;
270-
let mut err = String::new();
271-
let table = parsed
272-
.as_table()
273-
.ok_or_else(|| String::from("Parsed config was not table"))?;
274-
for key in table.keys() {
275-
if !Config::is_valid_name(key) {
276-
let msg = &format!("Warning: Unknown configuration option `{}`\n", key);
277-
err.push_str(msg)
278-
}
279-
}
280-
match parsed.try_into() {
281-
Ok(parsed_config) => {
282-
if !err.is_empty() {
283-
eprint!("{}", err);
284-
}
285-
let config = Config::default().fill_from_parsed_config(parsed_config, dir);
286-
Ok(config)
287-
}
288-
Err(e) => {
289-
err.push_str("Error: Decoding config file failed:\n");
290-
err.push_str(format!("{}\n", e).as_str());
291-
err.push_str("Please check your config file.");
292-
Err(err)
293-
}
294-
}
315+
let partial_config = PartialConfig::from_toml(toml)?;
316+
let config = Config::default().fill_from_parsed_config(partial_config, dir);
317+
Ok(config)
295318
}
296319
}
297320

@@ -300,14 +323,14 @@ impl Config {
300323
pub fn load_config<O: CliOptions>(
301324
file_path: Option<&Path>,
302325
options: Option<&O>,
303-
) -> Result<(Config, Option<PathBuf>), Error> {
326+
) -> Result<(Config, Option<Vec<PathBuf>>), Error> {
304327
let over_ride = match options {
305328
Some(opts) => config_path(opts)?,
306329
None => None,
307330
};
308331

309332
let result = if let Some(over_ride) = over_ride {
310-
Config::from_toml_path(over_ride.as_ref()).map(|p| (p, Some(over_ride.to_owned())))
333+
Config::from_toml_path(over_ride.as_ref()).map(|p| (p, Some(vec![over_ride.to_owned()])))
311334
} else if let Some(file_path) = file_path {
312335
Config::from_resolved_toml_path(file_path)
313336
} else {
@@ -417,6 +440,42 @@ mod test {
417440
}
418441
}
419442

443+
struct TempFile {
444+
path: PathBuf,
445+
}
446+
447+
fn make_temp_file(file_name: &'static str, content: &'static str) -> TempFile {
448+
use std::env::var;
449+
450+
// Used in the Rust build system.
451+
let target_dir = var("RUSTFMT_TEST_DIR").map_or_else(|_| env::temp_dir(), PathBuf::from);
452+
let path = target_dir.join(file_name);
453+
454+
fs::create_dir_all(path.parent().unwrap()).expect("couldn't create temp file");
455+
let mut file = File::create(&path).expect("couldn't create temp file");
456+
file.write_all(content.as_bytes())
457+
.expect("couldn't write temp file");
458+
TempFile { path }
459+
}
460+
461+
impl Drop for TempFile {
462+
fn drop(&mut self) {
463+
use std::fs::remove_file;
464+
remove_file(&self.path).expect("couldn't delete temp file");
465+
}
466+
}
467+
468+
struct NullOptions;
469+
470+
impl CliOptions for NullOptions {
471+
fn apply_to(&self, _: &mut Config) {
472+
unreachable!();
473+
}
474+
fn config_path(&self) -> Option<&Path> {
475+
unreachable!();
476+
}
477+
}
478+
420479
#[test]
421480
fn test_config_set() {
422481
let mut config = Config::default();
@@ -568,6 +627,37 @@ ignore = []
568627
assert_eq!(&toml, &default_config);
569628
}
570629

630+
#[test]
631+
fn test_merged_config() {
632+
let _outer_config = make_temp_file(
633+
"a/rustfmt.toml",
634+
r#"
635+
tab_spaces = 2
636+
fn_call_width = 50
637+
ignore = ["b/main.rs", "util.rs"]
638+
"#,
639+
);
640+
641+
let inner_config = make_temp_file(
642+
"a/b/rustfmt.toml",
643+
r#"
644+
tab_spaces = 3
645+
ignore = []
646+
"#,
647+
);
648+
649+
let inner_dir = inner_config.path.parent().unwrap();
650+
let (config, paths) = load_config::<NullOptions>(Some(inner_dir), None).unwrap();
651+
652+
assert_eq!(config.tab_spaces(), 3);
653+
assert_eq!(config.fn_call_width(), 50);
654+
assert_eq!(config.ignore().to_string(), r#"["main.rs"]"#);
655+
656+
let paths = paths.unwrap();
657+
assert!(paths[0].ends_with("a/rustfmt.toml"));
658+
assert!(paths[1].ends_with("a/b/rustfmt.toml"));
659+
}
660+
571661
mod unstable_features {
572662
use super::super::*;
573663

rustfmt-core/rustfmt-lib/src/config/config_type.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ impl ConfigType for IgnoreList {
5050
}
5151
}
5252

53+
macro_rules! update {
54+
($self:ident, ignore = $val:ident, $dir:ident) => {
55+
$self.ignore.1 = true;
56+
57+
let mut new_ignored = $val;
58+
new_ignored.add_prefix($dir);
59+
let old_ignored = $self.ignore.2;
60+
$self.ignore.2 = old_ignored.merge_into(new_ignored);
61+
};
62+
63+
($self:ident, $i:ident = $val:ident, $dir:ident) => {
64+
$self.$i.1 = true;
65+
$self.$i.2 = $val;
66+
};
67+
}
68+
5369
macro_rules! create_config {
5470
($($i:ident: $Ty:ty, $def:expr, $is_stable:literal, $dstring:literal;)+) => (
5571
use std::io::Write;
@@ -149,12 +165,10 @@ macro_rules! create_config {
149165
$(
150166
if let Some(val) = parsed.$i {
151167
if self.$i.3 {
152-
self.$i.1 = true;
153-
self.$i.2 = val;
168+
update!(self, $i = val, dir);
154169
} else {
155170
if is_nightly_channel!() {
156-
self.$i.1 = true;
157-
self.$i.2 = val;
171+
update!(self, $i = val, dir);
158172
} else {
159173
eprintln!("Warning: can't set `{} = {:?}`, unstable features are only \
160174
available in nightly channel.", stringify!($i), val);
@@ -164,7 +178,6 @@ macro_rules! create_config {
164178
)+
165179
self.set_heuristics();
166180
self.set_license_template();
167-
self.set_ignore(dir);
168181
self
169182
}
170183

@@ -397,10 +410,6 @@ macro_rules! create_config {
397410
}
398411
}
399412

400-
fn set_ignore(&mut self, dir: &Path) {
401-
self.ignore.2.add_prefix(dir);
402-
}
403-
404413
#[allow(unreachable_pub)]
405414
/// Returns `true` if the config key was explicitly set and is the default value.
406415
pub fn is_default(&self, key: &str) -> bool {

0 commit comments

Comments
 (0)