fix(config): combine key error context into one (#16004)

### What does this PR try to resolve?

This is a cleanup of annoying multiple "failed to parse key".

```
Caused by:
  failed to parse key `alias`

Caused by:
  failed to parse key `nested`
```

to

```
Caused by:
  failed to parse config at path `alias.nested`
```

This also open a door to using annotate-snippets to get the span of a
key path.

### How to test and review this PR?

This controversial part is we are using the invalid syntax to represent
the key path.
For example, `alias.nested[2]` refers to

```toml
[alias]
nested = ["a", "b", "c"]
#                   ^^^ this one
```

r? Muscraft
This commit is contained in:
Ed Page 2025-09-24 16:56:26 +00:00 committed by GitHub
commit b41a16c1ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 50 additions and 20 deletions

View File

@ -112,7 +112,7 @@ impl fmt::Display for ConfigKey {
}
}
fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> {
pub(super) fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> {
let ok = part.chars().all(|c| {
matches!(c,
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')

View File

@ -2155,6 +2155,12 @@ impl From<anyhow::Error> for ConfigError {
}
}
#[derive(Debug)]
enum KeyOrIdx {
Key(String),
Idx(usize),
}
#[derive(Eq, PartialEq, Clone)]
pub enum ConfigValue {
Integer(i64, Definition),
@ -2197,26 +2203,56 @@ impl ConfigValue {
}
fn from_toml(def: Definition, toml: toml::Value) -> CargoResult<ConfigValue> {
let mut error_path = Vec::new();
Self::from_toml_inner(def, toml, &mut error_path).with_context(|| {
let mut it = error_path.iter().rev().peekable();
let mut key_path = String::with_capacity(error_path.len() * 3);
while let Some(k) = it.next() {
match k {
KeyOrIdx::Key(s) => key_path.push_str(&key::escape_key_part(&s)),
KeyOrIdx::Idx(i) => key_path.push_str(&format!("[{i}]")),
}
if matches!(it.peek(), Some(KeyOrIdx::Key(_))) {
key_path.push('.');
}
}
format!("failed to parse config at `{key_path}`")
})
}
fn from_toml_inner(
def: Definition,
toml: toml::Value,
path: &mut Vec<KeyOrIdx>,
) -> CargoResult<ConfigValue> {
match toml {
toml::Value::String(val) => Ok(CV::String(val, def)),
toml::Value::Boolean(b) => Ok(CV::Boolean(b, def)),
toml::Value::Integer(i) => Ok(CV::Integer(i, def)),
toml::Value::Array(val) => Ok(CV::List(
val.into_iter()
.map(|toml| match toml {
.enumerate()
.map(|(i, toml)| match toml {
toml::Value::String(val) => Ok((val, def.clone())),
v => bail!("expected string but found {} in list", v.type_str()),
v => {
path.push(KeyOrIdx::Idx(i));
bail!("expected string but found {} at index {i}", v.type_str())
}
})
.collect::<CargoResult<_>>()?,
def,
)),
toml::Value::Table(val) => Ok(CV::Table(
val.into_iter()
.map(|(key, value)| {
let value = CV::from_toml(def.clone(), value)
.with_context(|| format!("failed to parse key `{}`", key))?;
Ok((key, value))
})
.map(
|(key, value)| match CV::from_toml_inner(def.clone(), value, path) {
Ok(value) => Ok((key, value)),
Err(e) => {
path.push(KeyOrIdx::Key(key));
Err(e)
}
},
)
.collect::<CargoResult<_>>()?,
def,
)),

View File

@ -47,10 +47,7 @@ Caused by:
failed to load TOML configuration from `[ROOT]/foo/.cargo/config.toml`
Caused by:
failed to parse key `http`
Caused by:
failed to parse key `proxy`
failed to parse config at `http.proxy`
Caused by:
found TOML configuration value of unknown type `float`

View File

@ -87,13 +87,10 @@ Caused by:
failed to load TOML configuration from `[ROOT]/foo/.cargo/config.toml`
Caused by:
failed to parse key `alias`
failed to parse config at `alias.b-cargo-test[0]`
Caused by:
failed to parse key `b-cargo-test`
Caused by:
expected string but found integer in list
expected string but found integer at index 0
"#]])
.run();

View File

@ -1363,10 +1363,10 @@ Caused by:
failed to load TOML configuration from `[ROOT]/.cargo/config.toml`
Caused by:
failed to parse key `foo`
failed to parse config at `foo[0]`
Caused by:
expected string but found integer in list
expected string but found integer at index 0
"#]],
);
}

View File

@ -527,7 +527,7 @@ fn bad_cv_convert() {
failed to convert --config argument `a=2019-12-01`
Caused by:
failed to parse key `a`
failed to parse config at `a`
Caused by:
found TOML configuration value of unknown type `datetime`