Skip to content

Commit f3c72eb

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 27d5871 commit f3c72eb

File tree

4 files changed

+234
-65
lines changed

4 files changed

+234
-65
lines changed

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

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

425425
enum FileConfig {
426426
Default,
427-
Local(Config, Option<PathBuf>),
427+
Local(Config, Option<Vec<PathBuf>>),
428428
}
429429

430430
struct FileConfigPair<'a> {
@@ -456,9 +456,9 @@ impl<'a> Iterator for FileConfigPairIter<'a> {
456456
let config = if self.has_config_from_commandline {
457457
FileConfig::Default
458458
} else {
459-
let (local_config, config_path) =
459+
let (local_config, config_paths) =
460460
load_config(Some(file.parent()?), Some(self.opt)).ok()?;
461-
FileConfig::Local(local_config, config_path)
461+
FileConfig::Local(local_config, config_paths)
462462
};
463463

464464
Some(FileConfigPair { file, config })
@@ -482,26 +482,32 @@ fn format(opt: Opt) -> Result<i32> {
482482
return Err(format_err!("Error: `{}` is a directory", dir.display()));
483483
}
484484

485-
let (config, config_path) = load_config(None, Some(&opt))?;
485+
let (config, config_paths) = load_config(None, Some(&opt))?;
486486

487487
if config.verbose() == Verbosity::Verbose {
488-
if let Some(path) = config_path.as_ref() {
489-
println!("Using rustfmt config file {}", path.display());
488+
if let Some(paths) = config_paths.as_ref() {
489+
for path in paths.iter() {
490+
println!("Using rustfmt config file {}", path.display());
491+
}
490492
}
491493
}
492494

493495
let out = &mut stdout();
494496
let mut session = Session::new(config, Some(out));
495497

496-
for pair in FileConfigPairIter::new(&opt, config_path.is_some()) {
498+
for pair in FileConfigPairIter::new(&opt, config_paths.is_some()) {
497499
let file = pair.file;
498500

499501
if let FileConfig::Local(local_config, config_path) = pair.config {
500-
if let Some(path) = config_path {
502+
if let Some(paths) = config_path {
501503
if local_config.verbose() == Verbosity::Verbose {
502504
println!(
503-
"Using rustfmt config file {} for {}",
504-
path.display(),
505+
"Using rustfmt config files {} for {}",
506+
paths
507+
.into_iter()
508+
.map(|p| p.display().to_string())
509+
.collect::<Vec<_>>()
510+
.join(","),
505511
file.display()
506512
);
507513
}

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

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

185185
::toml::to_string(&cloned).map_err(ToTomlError)
186186
}
187+
188+
pub fn from_toml_path(file_path: &Path) -> Result<PartialConfig, Error> {
189+
let mut file = File::open(&file_path)?;
190+
let mut toml = String::new();
191+
file.read_to_string(&mut toml)?;
192+
PartialConfig::from_toml(&toml).map_err(|err| Error::new(ErrorKind::InvalidData, err))
193+
}
194+
195+
fn from_toml(toml: &str) -> Result<PartialConfig, String> {
196+
let parsed: ::toml::Value = toml
197+
.parse()
198+
.map_err(|e| format!("Could not parse TOML: {}", e))?;
199+
let mut err = String::new();
200+
let table = parsed
201+
.as_table()
202+
.ok_or_else(|| String::from("Parsed config was not table"))?;
203+
for key in table.keys() {
204+
if !Config::is_valid_name(key) {
205+
let msg = &format!("Warning: Unknown configuration option `{}`\n", key);
206+
err.push_str(msg)
207+
}
208+
}
209+
match parsed.try_into() {
210+
Ok(parsed_config) => {
211+
if !err.is_empty() {
212+
eprint!("{}", err);
213+
}
214+
Ok(parsed_config)
215+
}
216+
Err(e) => {
217+
err.push_str("Error: Decoding config file failed:\n");
218+
err.push_str(format!("{}\n", e).as_str());
219+
err.push_str("Please check your config file.");
220+
Err(err)
221+
}
222+
}
223+
}
187224
}
188225

189226
impl Config {
@@ -211,11 +248,8 @@ impl Config {
211248
/// Returns a `Config` if the config could be read and parsed from
212249
/// the file, otherwise errors.
213250
pub fn from_toml_path(file_path: &Path) -> Result<Config, Error> {
214-
let mut file = File::open(&file_path)?;
215-
let mut toml = String::new();
216-
file.read_to_string(&mut toml)?;
217-
Config::from_toml(&toml, file_path.parent().unwrap())
218-
.map_err(|err| Error::new(ErrorKind::InvalidData, err))
251+
let partial_config = PartialConfig::from_toml_path(file_path)?;
252+
Ok(Config::default().fill_from_parsed_config(partial_config, file_path.parent().unwrap()))
219253
}
220254

221255
/// Resolves the config for input in `dir`.
@@ -227,85 +261,74 @@ impl Config {
227261
///
228262
/// Returns the `Config` to use, and the path of the project file if there was
229263
/// one.
230-
pub fn from_resolved_toml_path(dir: &Path) -> Result<(Config, Option<PathBuf>), Error> {
264+
pub fn from_resolved_toml_path(dir: &Path) -> Result<(Config, Option<Vec<PathBuf>>), Error> {
231265
/// Try to find a project file in the given directory and its parents.
232266
/// Returns the path of a the nearest project file if one exists,
233267
/// or `None` if no project file was found.
234-
fn resolve_project_file(dir: &Path) -> Result<Option<PathBuf>, Error> {
268+
fn resolve_project_files(dir: &Path) -> Result<Option<Vec<PathBuf>>, Error> {
235269
let mut current = if dir.is_relative() {
236270
env::current_dir()?.join(dir)
237271
} else {
238272
dir.to_path_buf()
239273
};
240274

241275
current = dunce::canonicalize(current)?;
276+
let mut paths = Vec::new();
242277

243278
loop {
244-
match get_toml_path(&current) {
245-
Ok(Some(path)) => return Ok(Some(path)),
246-
Err(e) => return Err(e),
247-
_ => (),
248-
}
279+
let current_toml_path = get_toml_path(&current)?;
280+
paths.push(current_toml_path);
249281

250282
// If the current directory has no parent, we're done searching.
251283
if !current.pop() {
252284
break;
253285
}
254286
}
255287

288+
if !paths.is_empty() {
289+
return Ok(paths.into_iter().filter(|p| p.is_some()).collect());
290+
}
291+
256292
// If nothing was found, check in the home directory.
257293
if let Some(home_dir) = dirs::home_dir() {
258294
if let Some(path) = get_toml_path(&home_dir)? {
259-
return Ok(Some(path));
295+
return Ok(Some(vec![path]));
260296
}
261297
}
262298

263299
// If none was found ther either, check in the user's configuration directory.
264300
if let Some(mut config_dir) = dirs::config_dir() {
265301
config_dir.push("rustfmt");
266302
if let Some(path) = get_toml_path(&config_dir)? {
267-
return Ok(Some(path));
303+
return Ok(Some(vec![path]));
268304
}
269305
}
270306

271307
Ok(None)
272308
}
273309

274-
match resolve_project_file(dir)? {
310+
let files = resolve_project_files(dir);
311+
312+
match files? {
275313
None => Ok((Config::default(), None)),
276-
Some(path) => Config::from_toml_path(&path).map(|config| (config, Some(path))),
314+
Some(paths) => {
315+
let mut config = Config::default();
316+
let mut used_paths = Vec::with_capacity(paths.len());
317+
for path in paths.into_iter().rev() {
318+
let partial_config = PartialConfig::from_toml_path(&path)?;
319+
config = config.fill_from_parsed_config(partial_config, &path);
320+
used_paths.push(path);
321+
}
322+
323+
Ok((config, Some(used_paths)))
324+
}
277325
}
278326
}
279327

280328
pub fn from_toml(toml: &str, dir: &Path) -> Result<Config, String> {
281-
let parsed: ::toml::Value = toml
282-
.parse()
283-
.map_err(|e| format!("Could not parse TOML: {}", e))?;
284-
let mut err = String::new();
285-
let table = parsed
286-
.as_table()
287-
.ok_or_else(|| String::from("Parsed config was not table"))?;
288-
for key in table.keys() {
289-
if !Config::is_valid_name(key) {
290-
let msg = &format!("Warning: Unknown configuration option `{}`\n", key);
291-
err.push_str(msg)
292-
}
293-
}
294-
match parsed.try_into() {
295-
Ok(parsed_config) => {
296-
if !err.is_empty() {
297-
eprint!("{}", err);
298-
}
299-
let config = Config::default().fill_from_parsed_config(parsed_config, dir);
300-
Ok(config)
301-
}
302-
Err(e) => {
303-
err.push_str("Error: Decoding config file failed:\n");
304-
err.push_str(format!("{}\n", e).as_str());
305-
err.push_str("Please check your config file.");
306-
Err(err)
307-
}
308-
}
329+
let partial_config = PartialConfig::from_toml(toml)?;
330+
let config = Config::default().fill_from_parsed_config(partial_config, dir);
331+
Ok(config)
309332
}
310333
}
311334

@@ -314,14 +337,14 @@ impl Config {
314337
pub fn load_config<O: CliOptions>(
315338
file_path: Option<&Path>,
316339
options: Option<&O>,
317-
) -> Result<(Config, Option<PathBuf>), Error> {
340+
) -> Result<(Config, Option<Vec<PathBuf>>), Error> {
318341
let over_ride = match options {
319342
Some(opts) => config_path(opts)?,
320343
None => None,
321344
};
322345

323346
let result = if let Some(over_ride) = over_ride {
324-
Config::from_toml_path(over_ride.as_ref()).map(|p| (p, Some(over_ride.to_owned())))
347+
Config::from_toml_path(over_ride.as_ref()).map(|p| (p, Some(vec![over_ride.to_owned()])))
325348
} else if let Some(file_path) = file_path {
326349
Config::from_resolved_toml_path(file_path)
327350
} else {
@@ -433,6 +456,42 @@ mod test {
433456
}
434457
}
435458

459+
struct TempFile {
460+
path: PathBuf,
461+
}
462+
463+
fn make_temp_file(file_name: &'static str, content: &'static str) -> TempFile {
464+
use std::env::var;
465+
466+
// Used in the Rust build system.
467+
let target_dir = var("RUSTFMT_TEST_DIR").map_or_else(|_| env::temp_dir(), PathBuf::from);
468+
let path = target_dir.join(file_name);
469+
470+
fs::create_dir_all(path.parent().unwrap()).expect("couldn't create temp file");
471+
let mut file = File::create(&path).expect("couldn't create temp file");
472+
file.write_all(content.as_bytes())
473+
.expect("couldn't write temp file");
474+
TempFile { path }
475+
}
476+
477+
impl Drop for TempFile {
478+
fn drop(&mut self) {
479+
use std::fs::remove_file;
480+
remove_file(&self.path).expect("couldn't delete temp file");
481+
}
482+
}
483+
484+
struct NullOptions;
485+
486+
impl CliOptions for NullOptions {
487+
fn apply_to(&self, _: &mut Config) {
488+
unreachable!();
489+
}
490+
fn config_path(&self) -> Option<&Path> {
491+
unreachable!();
492+
}
493+
}
494+
436495
#[test]
437496
fn test_config_set() {
438497
let mut config = Config::default();
@@ -585,6 +644,37 @@ ignore = []
585644
assert_eq!(&toml, &default_config);
586645
}
587646

647+
#[test]
648+
fn test_merged_config() {
649+
let _outer_config = make_temp_file(
650+
"a/rustfmt.toml",
651+
r#"
652+
tab_spaces = 2
653+
fn_call_width = 50
654+
ignore = ["b/main.rs", "util.rs"]
655+
"#,
656+
);
657+
658+
let inner_config = make_temp_file(
659+
"a/b/rustfmt.toml",
660+
r#"
661+
tab_spaces = 3
662+
ignore = []
663+
"#,
664+
);
665+
666+
let inner_dir = inner_config.path.parent().unwrap();
667+
let (config, paths) = load_config::<NullOptions>(Some(inner_dir), None).unwrap();
668+
669+
assert_eq!(config.tab_spaces(), 3);
670+
assert_eq!(config.fn_call_width(), 50);
671+
assert_eq!(config.ignore().to_string(), r#"["main.rs"]"#);
672+
673+
let paths = paths.unwrap();
674+
assert!(paths[0].ends_with("a/rustfmt.toml"));
675+
assert!(paths[1].ends_with("a/b/rustfmt.toml"));
676+
}
677+
588678
mod unstable_features {
589679
use super::super::*;
590680

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

@@ -398,10 +411,6 @@ macro_rules! create_config {
398411
}
399412
}
400413

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

0 commit comments

Comments
 (0)