Skip to content

Commit 796cc58

Browse files
authored
Merge configs from parent directories (#4179)
1 parent fd1362a commit 796cc58

File tree

5 files changed

+253
-68
lines changed

5 files changed

+253
-68
lines changed

ci/integration.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,27 @@ function show_head {
7171
echo "Head commit of ${INTEGRATION}: $head"
7272
}
7373

74+
tempdir=$(mktemp -d)
75+
7476
case ${INTEGRATION} in
7577
cargo)
76-
git clone --depth=1 https://github.com/rust-lang/${INTEGRATION}.git
77-
cd ${INTEGRATION}
78+
git clone --depth=1 https://github.com/rust-lang/${INTEGRATION}.git ${tempdir}
79+
cd ${tempdir}
7880
show_head
7981
export CFG_DISABLE_CROSS_TESTS=1
8082
check_fmt_with_all_tests
8183
cd -
8284
;;
8385
crater)
84-
git clone --depth=1 https://github.com/rust-lang-nursery/${INTEGRATION}.git
85-
cd ${INTEGRATION}
86+
git clone --depth=1 https://github.com/rust-lang-nursery/${INTEGRATION}.git ${tempdir}
87+
cd ${tempdir}
8688
show_head
8789
check_fmt_with_lib_tests
8890
cd -
8991
;;
9092
*)
91-
git clone --depth=1 https://github.com/rust-lang-nursery/${INTEGRATION}.git
92-
cd ${INTEGRATION}
93+
git clone --depth=1 https://github.com/rust-lang-nursery/${INTEGRATION}.git ${tempdir}
94+
cd ${tempdir}
9395
show_head
9496
check_fmt_with_all_tests
9597
cd -

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

Lines changed: 14 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,18 @@ 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 file(s) {}",
492+
paths
493+
.into_iter()
494+
.map(|p| p.display().to_string())
495+
.collect::<Vec<_>>()
496+
.join(","),
497+
);
491498
}
492499
}
493500

@@ -496,7 +503,7 @@ fn format(opt: Opt) -> Result<i32> {
496503
verbosity: opt.verbosity(),
497504
};
498505

499-
let inputs = FileConfigPairIter::new(&opt, config_path.is_some()).collect::<Vec<_>>();
506+
let inputs = FileConfigPairIter::new(&opt, config_paths.is_some()).collect::<Vec<_>>();
500507
let format_report = format_inputs(
501508
inputs.iter().map(|p| {
502509
(

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

Lines changed: 146 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,77 @@ 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+
// List of closest -> most distant rustfmt config from the current directory.
275+
let config_paths: Option<Vec<_>> = paths.into_iter().filter(|p| p.is_some()).collect();
276+
let has_paths = config_paths
277+
.as_ref()
278+
.map_or(false, |paths| !paths.is_empty());
279+
if has_paths {
280+
return Ok(config_paths);
281+
}
282+
242283
// If nothing was found, check in the home directory.
243284
if let Some(home_dir) = dirs::home_dir() {
244285
if let Some(path) = get_toml_path(&home_dir)? {
245-
return Ok(Some(path));
286+
return Ok(Some(vec![path]));
246287
}
247288
}
248289

249290
// If none was found ther either, check in the user's configuration directory.
250291
if let Some(mut config_dir) = dirs::config_dir() {
251292
config_dir.push("rustfmt");
252293
if let Some(path) = get_toml_path(&config_dir)? {
253-
return Ok(Some(path));
294+
return Ok(Some(vec![path]));
254295
}
255296
}
256297

257298
Ok(None)
258299
}
259300

260-
match resolve_project_file(dir)? {
301+
match resolve_project_files(dir)? {
261302
None => Ok((Config::default(), None)),
262-
Some(path) => Config::from_toml_path(&path).map(|config| (config, Some(path))),
303+
Some(paths) => {
304+
let mut config = Config::default();
305+
let mut used_paths = Vec::with_capacity(paths.len());
306+
for path in paths.into_iter().rev() {
307+
let partial_config = PartialConfig::from_toml_path(&path)?;
308+
config = config.fill_from_parsed_config(partial_config, &path);
309+
used_paths.push(path);
310+
}
311+
312+
Ok((config, Some(used_paths)))
313+
}
263314
}
264315
}
265316

266317
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-
}
318+
let partial_config = PartialConfig::from_toml(toml)?;
319+
let config = Config::default().fill_from_parsed_config(partial_config, dir);
320+
Ok(config)
295321
}
296322
}
297323

@@ -300,14 +326,14 @@ impl Config {
300326
pub fn load_config<O: CliOptions>(
301327
file_path: Option<&Path>,
302328
options: Option<&O>,
303-
) -> Result<(Config, Option<PathBuf>), Error> {
329+
) -> Result<(Config, Option<Vec<PathBuf>>), Error> {
304330
let over_ride = match options {
305331
Some(opts) => config_path(opts)?,
306332
None => None,
307333
};
308334

309335
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())))
336+
Config::from_toml_path(over_ride.as_ref()).map(|p| (p, Some(vec![over_ride.to_owned()])))
311337
} else if let Some(file_path) = file_path {
312338
Config::from_resolved_toml_path(file_path)
313339
} else {
@@ -417,6 +443,42 @@ mod test {
417443
}
418444
}
419445

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

633+
#[test]
634+
fn test_merged_config() {
635+
match option_env!("CFG_RELEASE_CHANNEL") {
636+
// this test requires nightly
637+
None | Some("nightly") => {
638+
let _outer_config = make_temp_file(
639+
"a/rustfmt.toml",
640+
r#"
641+
tab_spaces = 2
642+
fn_call_width = 50
643+
ignore = ["b/main.rs", "util.rs"]
644+
"#,
645+
);
646+
647+
let inner_config = make_temp_file(
648+
"a/b/rustfmt.toml",
649+
r#"
650+
version = "two"
651+
tab_spaces = 3
652+
ignore = []
653+
"#,
654+
);
655+
656+
let inner_dir = inner_config.path.parent().unwrap();
657+
let (config, paths) = load_config::<NullOptions>(Some(inner_dir), None).unwrap();
658+
659+
assert_eq!(config.tab_spaces(), 3);
660+
assert_eq!(config.fn_call_width(), 50);
661+
assert_eq!(config.ignore().to_string(), r#"["main.rs"]"#);
662+
663+
let paths = paths.unwrap();
664+
assert!(paths[0].ends_with("a/rustfmt.toml"));
665+
assert!(paths[1].ends_with("a/b/rustfmt.toml"));
666+
}
667+
_ => (),
668+
};
669+
}
670+
571671
mod unstable_features {
572672
use super::super::*;
573673

0 commit comments

Comments
 (0)