Skip to content

Commit 9304fdf

Browse files
purajitMichaReiser
andauthored
better error messages while loading configuration extends (#15658)
Co-authored-by: Micha Reiser <[email protected]>
1 parent 0babbca commit 9304fdf

File tree

5 files changed

+182
-16
lines changed

5 files changed

+182
-16
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff/tests/format.rs

+2
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ if True:
816816
817817
----- stderr -----
818818
ruff failed
819+
Cause: Failed to load configuration `[RUFF-TOML-PATH]`
819820
Cause: Failed to parse [RUFF-TOML-PATH]
820821
Cause: TOML parse error at line 1, column 1
821822
|
@@ -855,6 +856,7 @@ format = "json"
855856
856857
----- stderr -----
857858
ruff failed
859+
Cause: Failed to load configuration `[RUFF-TOML-PATH]`
858860
Cause: Failed to parse [RUFF-TOML-PATH]
859861
Cause: TOML parse error at line 2, column 10
860862
|

crates/ruff/tests/lint.rs

+138-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use tempfile::TempDir;
1515
const BIN_NAME: &str = "ruff";
1616
const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"];
1717

18-
fn tempdir_filter(tempdir: &TempDir) -> String {
19-
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
18+
fn tempdir_filter(path: impl AsRef<Path>) -> String {
19+
format!(r"{}\\?/?", escape(path.as_ref().to_str().unwrap()))
2020
}
2121

2222
#[test]
@@ -609,6 +609,139 @@ fn extend_passed_via_config_argument() {
609609
");
610610
}
611611

612+
#[test]
613+
fn nonexistent_extend_file() -> Result<()> {
614+
let tempdir = TempDir::new()?;
615+
let project_dir = tempdir.path().canonicalize()?;
616+
fs::write(
617+
project_dir.join("ruff.toml"),
618+
r#"
619+
extend = "ruff2.toml"
620+
"#,
621+
)?;
622+
623+
fs::write(
624+
project_dir.join("ruff2.toml"),
625+
r#"
626+
extend = "ruff3.toml"
627+
"#,
628+
)?;
629+
630+
insta::with_settings!({
631+
filters => vec![
632+
(tempdir_filter(&project_dir).as_str(), "[TMP]/"),
633+
("The system cannot find the file specified.", "No such file or directory")
634+
]
635+
}, {
636+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
637+
.args(["check"]).current_dir(project_dir), @r"
638+
success: false
639+
exit_code: 2
640+
----- stdout -----
641+
642+
----- stderr -----
643+
ruff failed
644+
Cause: Failed to load extended configuration `[TMP]/ruff3.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml`)
645+
Cause: Failed to read [TMP]/ruff3.toml
646+
Cause: No such file or directory (os error 2)
647+
");
648+
});
649+
650+
Ok(())
651+
}
652+
653+
#[test]
654+
fn circular_extend() -> Result<()> {
655+
let tempdir = TempDir::new()?;
656+
let project_path = tempdir.path().canonicalize()?;
657+
658+
fs::write(
659+
project_path.join("ruff.toml"),
660+
r#"
661+
extend = "ruff2.toml"
662+
"#,
663+
)?;
664+
fs::write(
665+
project_path.join("ruff2.toml"),
666+
r#"
667+
extend = "ruff3.toml"
668+
"#,
669+
)?;
670+
fs::write(
671+
project_path.join("ruff3.toml"),
672+
r#"
673+
extend = "ruff.toml"
674+
"#,
675+
)?;
676+
677+
insta::with_settings!({
678+
filters => vec![(tempdir_filter(&project_path).as_str(), "[TMP]/")]
679+
}, {
680+
assert_cmd_snapshot!(
681+
Command::new(get_cargo_bin(BIN_NAME))
682+
.args(["check"])
683+
.current_dir(project_path),
684+
@r"
685+
success: false
686+
exit_code: 2
687+
----- stdout -----
688+
689+
----- stderr -----
690+
ruff failed
691+
Cause: Circular configuration detected: `[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml` extends `[TMP]/ruff.toml`
692+
");
693+
});
694+
695+
Ok(())
696+
}
697+
698+
#[test]
699+
fn parse_error_extends() -> Result<()> {
700+
let tempdir = TempDir::new()?;
701+
let project_path = tempdir.path().canonicalize()?;
702+
703+
fs::write(
704+
project_path.join("ruff.toml"),
705+
r#"
706+
extend = "ruff2.toml"
707+
"#,
708+
)?;
709+
fs::write(
710+
project_path.join("ruff2.toml"),
711+
r#"
712+
[lint]
713+
select = [E501]
714+
"#,
715+
)?;
716+
717+
insta::with_settings!({
718+
filters => vec![(tempdir_filter(&project_path).as_str(), "[TMP]/")]
719+
}, {
720+
assert_cmd_snapshot!(
721+
Command::new(get_cargo_bin(BIN_NAME))
722+
.args(["check"])
723+
.current_dir(project_path),
724+
@r"
725+
success: false
726+
exit_code: 2
727+
----- stdout -----
728+
729+
----- stderr -----
730+
ruff failed
731+
Cause: Failed to load extended configuration `[TMP]/ruff2.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml`)
732+
Cause: Failed to parse [TMP]/ruff2.toml
733+
Cause: TOML parse error at line 3, column 11
734+
|
735+
3 | select = [E501]
736+
| ^
737+
invalid array
738+
expected `]`
739+
");
740+
});
741+
742+
Ok(())
743+
}
744+
612745
#[test]
613746
fn config_file_and_isolated() -> Result<()> {
614747
let tempdir = TempDir::new()?;
@@ -2095,6 +2228,7 @@ fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> {
20952228
20962229
----- stderr -----
20972230
ruff failed
2231+
Cause: Failed to load configuration `[TMP]/ruff.toml`
20982232
Cause: Failed to parse [TMP]/ruff.toml
20992233
Cause: TOML parse error at line 3, column 17
21002234
|
@@ -2131,6 +2265,7 @@ fn flake8_import_convention_invalid_aliases_config_extend_alias_name() -> Result
21312265
21322266
----- stderr -----
21332267
ruff failed
2268+
Cause: Failed to load configuration `[TMP]/ruff.toml`
21342269
Cause: Failed to parse [TMP]/ruff.toml
21352270
Cause: TOML parse error at line 3, column 17
21362271
|
@@ -2167,6 +2302,7 @@ fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> {
21672302
21682303
----- stderr -----
21692304
ruff failed
2305+
Cause: Failed to load configuration `[TMP]/ruff.toml`
21702306
Cause: Failed to parse [TMP]/ruff.toml
21712307
Cause: TOML parse error at line 3, column 1
21722308
|
@@ -2429,6 +2565,5 @@ fn a005_module_shadowing_strict_default() -> Result<()> {
24292565
----- stderr -----
24302566
");
24312567
});
2432-
24332568
Ok(())
24342569
}

crates/ruff_workspace/Cargo.toml

+8-2
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ ruff_macros = { workspace = true }
2121
ruff_python_ast = { workspace = true }
2222
ruff_python_formatter = { workspace = true, features = ["serde"] }
2323
ruff_python_semantic = { workspace = true, features = ["serde"] }
24-
ruff_python_stdlib = {workspace = true}
24+
ruff_python_stdlib = { workspace = true }
2525
ruff_source_file = { workspace = true }
2626

2727
anyhow = { workspace = true }
2828
colored = { workspace = true }
2929
ignore = { workspace = true }
30+
indexmap = { workspace = true }
3031
is-macro = { workspace = true }
3132
itertools = { workspace = true }
3233
log = { workspace = true }
@@ -58,7 +59,12 @@ ignored = ["colored"]
5859

5960
[features]
6061
default = []
61-
schemars = ["dep:schemars", "ruff_formatter/schemars", "ruff_python_formatter/schemars", "ruff_python_semantic/schemars"]
62+
schemars = [
63+
"dep:schemars",
64+
"ruff_formatter/schemars",
65+
"ruff_python_formatter/schemars",
66+
"ruff_python_semantic/schemars",
67+
]
6268

6369
[lints]
6470
workspace = true

crates/ruff_workspace/src/resolver.rs

+33-11
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use std::ffi::OsStr;
77
use std::path::{Path, PathBuf};
88
use std::sync::RwLock;
99

10-
use anyhow::Result;
1110
use anyhow::{anyhow, bail};
11+
use anyhow::{Context, Result};
1212
use globset::{Candidate, GlobSet};
1313
use ignore::{DirEntry, Error, ParallelVisitor, WalkBuilder, WalkState};
1414
use itertools::Itertools;
@@ -304,16 +304,39 @@ pub fn resolve_configuration(
304304
relativity: Relativity,
305305
transformer: &dyn ConfigurationTransformer,
306306
) -> Result<Configuration> {
307-
let mut seen = FxHashSet::default();
308-
let mut stack = vec![];
307+
let mut configurations = indexmap::IndexMap::new();
309308
let mut next = Some(fs::normalize_path(pyproject));
310309
while let Some(path) = next {
311-
if seen.contains(&path) {
312-
bail!("Circular dependency detected in pyproject.toml");
310+
if configurations.contains_key(&path) {
311+
bail!(format!(
312+
"Circular configuration detected: {chain}",
313+
chain = configurations
314+
.keys()
315+
.chain([&path])
316+
.map(|p| format!("`{}`", p.display()))
317+
.join(" extends "),
318+
));
313319
}
314320

315321
// Resolve the current path.
316-
let options = pyproject::load_options(&path)?;
322+
let options = pyproject::load_options(&path).with_context(|| {
323+
if configurations.is_empty() {
324+
format!(
325+
"Failed to load configuration `{path}`",
326+
path = path.display()
327+
)
328+
} else {
329+
let chain = configurations
330+
.keys()
331+
.chain([&path])
332+
.map(|p| format!("`{}`", p.display()))
333+
.join(" extends ");
334+
format!(
335+
"Failed to load extended configuration `{path}` ({chain})",
336+
path = path.display()
337+
)
338+
}
339+
})?;
317340

318341
let project_root = relativity.resolve(&path);
319342
let configuration = Configuration::from_options(options, Some(&path), project_root)?;
@@ -329,14 +352,13 @@ pub fn resolve_configuration(
329352

330353
// Keep track of (1) the paths we've already resolved (to avoid cycles), and (2)
331354
// the base configuration for every path.
332-
seen.insert(path);
333-
stack.push(configuration);
355+
configurations.insert(path, configuration);
334356
}
335357

336358
// Merge the configurations, in order.
337-
stack.reverse();
338-
let mut configuration = stack.pop().unwrap();
339-
while let Some(extend) = stack.pop() {
359+
let mut configurations = configurations.into_values();
360+
let mut configuration = configurations.next().unwrap();
361+
for extend in configurations {
340362
configuration = configuration.combine(extend);
341363
}
342364
Ok(transformer.transform(configuration))

0 commit comments

Comments
 (0)