Skip to content

Commit 97e73c7

Browse files
committed
Auto merge of #12574 - dtolnay-contrib:untagged, r=epage
Improve deserialization errors of untagged enums ### What does this PR try to resolve? ```toml # .cargo/config.toml [http] ssl-version.min = false ``` **Before:** ```console $ cargo check error: data did not match any variant of untagged enum SslVersionConfig ``` **After:** ```console $ cargo check error: error in /path/to/.cargo/config.toml: could not load config key `http.ssl-version` Caused by: error in /path/to/.cargo/config.toml: `http.ssl-version.min` expected a string, but found a boolean ``` ### How should we test and review this PR? The first commit adds tests showing the pre-existing error messages — mostly just _"data did not match any variant of untagged enum T"_ with no location information. The second commit replaces all `#[derive(Deserialize)] #[serde(untagged)]` with Deserialize impls based on https://docs.rs/serde-untagged/0.1, showing the effect on the error messages. Tested with `cargo test`, and by handwriting some bad .cargo/config.toml files and looking at the error produced by `rust-lang/cargo/target/release/cargo check`.
2 parents bfa04bb + 3871aec commit 97e73c7

File tree

6 files changed

+266
-15
lines changed

6 files changed

+266
-15
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ same-file = "1.0.6"
7777
security-framework = "2.9.2"
7878
semver = { version = "1.0.18", features = ["serde"] }
7979
serde = "1.0.188"
80+
serde-untagged = "0.1.0"
8081
serde-value = "0.7.0"
8182
serde_ignored = "0.1.9"
8283
serde_json = "1.0.104"
@@ -163,6 +164,7 @@ rand.workspace = true
163164
rustfix.workspace = true
164165
semver.workspace = true
165166
serde = { workspace = true, features = ["derive"] }
167+
serde-untagged.workspace = true
166168
serde-value.workspace = true
167169
serde_ignored.workspace = true
168170
serde_json = { workspace = true, features = ["raw_value"] }

src/cargo/util/config/mod.rs

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ use curl::easy::Easy;
8484
use lazycell::LazyCell;
8585
use serde::de::IntoDeserializer as _;
8686
use serde::Deserialize;
87+
use serde_untagged::UntaggedEnumVisitor;
8788
use time::OffsetDateTime;
8889
use toml_edit::Item;
8990
use url::Url;
@@ -2453,13 +2454,24 @@ impl CargoFutureIncompatConfig {
24532454
/// ssl-version.min = "tlsv1.2"
24542455
/// ssl-version.max = "tlsv1.3"
24552456
/// ```
2456-
#[derive(Clone, Debug, Deserialize, PartialEq)]
2457-
#[serde(untagged)]
2457+
#[derive(Clone, Debug, PartialEq)]
24582458
pub enum SslVersionConfig {
24592459
Single(String),
24602460
Range(SslVersionConfigRange),
24612461
}
24622462

2463+
impl<'de> Deserialize<'de> for SslVersionConfig {
2464+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2465+
where
2466+
D: serde::Deserializer<'de>,
2467+
{
2468+
UntaggedEnumVisitor::new()
2469+
.string(|single| Ok(SslVersionConfig::Single(single.to_owned())))
2470+
.map(|map| map.deserialize().map(SslVersionConfig::Range))
2471+
.deserialize(deserializer)
2472+
}
2473+
}
2474+
24632475
#[derive(Clone, Debug, Deserialize, PartialEq)]
24642476
pub struct SslVersionConfigRange {
24652477
pub min: Option<String>,
@@ -2493,13 +2505,24 @@ pub struct CargoSshConfig {
24932505
/// [build]
24942506
/// jobs = "default" # Currently only support "default".
24952507
/// ```
2496-
#[derive(Debug, Deserialize, Clone)]
2497-
#[serde(untagged)]
2508+
#[derive(Debug, Clone)]
24982509
pub enum JobsConfig {
24992510
Integer(i32),
25002511
String(String),
25012512
}
25022513

2514+
impl<'de> Deserialize<'de> for JobsConfig {
2515+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2516+
where
2517+
D: serde::Deserializer<'de>,
2518+
{
2519+
UntaggedEnumVisitor::new()
2520+
.i32(|int| Ok(JobsConfig::Integer(int)))
2521+
.string(|string| Ok(JobsConfig::String(string.to_owned())))
2522+
.deserialize(deserializer)
2523+
}
2524+
}
2525+
25032526
#[derive(Debug, Deserialize)]
25042527
#[serde(rename_all = "kebab-case")]
25052528
pub struct CargoBuildConfig {
@@ -2534,13 +2557,24 @@ pub struct BuildTargetConfig {
25342557
inner: Value<BuildTargetConfigInner>,
25352558
}
25362559

2537-
#[derive(Debug, Deserialize)]
2538-
#[serde(untagged)]
2560+
#[derive(Debug)]
25392561
enum BuildTargetConfigInner {
25402562
One(String),
25412563
Many(Vec<String>),
25422564
}
25432565

2566+
impl<'de> Deserialize<'de> for BuildTargetConfigInner {
2567+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2568+
where
2569+
D: serde::Deserializer<'de>,
2570+
{
2571+
UntaggedEnumVisitor::new()
2572+
.string(|one| Ok(BuildTargetConfigInner::One(one.to_owned())))
2573+
.seq(|many| many.deserialize().map(BuildTargetConfigInner::Many))
2574+
.deserialize(deserializer)
2575+
}
2576+
}
2577+
25442578
impl BuildTargetConfig {
25452579
/// Gets values of `build.target` as a list of strings.
25462580
pub fn values(&self, config: &Config) -> CargoResult<Vec<String>> {
@@ -2652,19 +2686,44 @@ where
26522686
deserializer.deserialize_option(ProgressVisitor)
26532687
}
26542688

2655-
#[derive(Debug, Deserialize)]
2656-
#[serde(untagged)]
2689+
#[derive(Debug)]
26572690
enum EnvConfigValueInner {
26582691
Simple(String),
26592692
WithOptions {
26602693
value: String,
2661-
#[serde(default)]
26622694
force: bool,
2663-
#[serde(default)]
26642695
relative: bool,
26652696
},
26662697
}
26672698

2699+
impl<'de> Deserialize<'de> for EnvConfigValueInner {
2700+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2701+
where
2702+
D: serde::Deserializer<'de>,
2703+
{
2704+
#[derive(Deserialize)]
2705+
struct WithOptions {
2706+
value: String,
2707+
#[serde(default)]
2708+
force: bool,
2709+
#[serde(default)]
2710+
relative: bool,
2711+
}
2712+
2713+
UntaggedEnumVisitor::new()
2714+
.string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned())))
2715+
.map(|map| {
2716+
let with_options: WithOptions = map.deserialize()?;
2717+
Ok(EnvConfigValueInner::WithOptions {
2718+
value: with_options.value,
2719+
force: with_options.force,
2720+
relative: with_options.relative,
2721+
})
2722+
})
2723+
.deserialize(deserializer)
2724+
}
2725+
}
2726+
26682727
#[derive(Debug, Deserialize)]
26692728
#[serde(transparent)]
26702729
pub struct EnvConfigValue {

src/cargo/util/toml/mod.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ use cargo_util::paths;
1212
use itertools::Itertools;
1313
use lazycell::LazyCell;
1414
use semver::{self, VersionReq};
15-
use serde::de::{self, Unexpected};
15+
use serde::de::{self, IntoDeserializer as _, Unexpected};
1616
use serde::ser;
1717
use serde::{Deserialize, Serialize};
18+
use serde_untagged::UntaggedEnumVisitor;
1819
use tracing::{debug, trace};
1920
use url::Url;
2021

@@ -961,13 +962,25 @@ impl StringOrVec {
961962
}
962963
}
963964

964-
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
965-
#[serde(untagged, expecting = "expected a boolean or a string")]
965+
#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
966+
#[serde(untagged)]
966967
pub enum StringOrBool {
967968
String(String),
968969
Bool(bool),
969970
}
970971

972+
impl<'de> Deserialize<'de> for StringOrBool {
973+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
974+
where
975+
D: de::Deserializer<'de>,
976+
{
977+
UntaggedEnumVisitor::new()
978+
.string(|s| Ok(StringOrBool::String(s.to_owned())))
979+
.bool(|b| Ok(StringOrBool::Bool(b)))
980+
.deserialize(deserializer)
981+
}
982+
}
983+
971984
#[derive(PartialEq, Clone, Debug, Serialize)]
972985
#[serde(untagged)]
973986
pub enum VecStringOrBool {
@@ -3513,13 +3526,27 @@ pub type TomlLints = BTreeMap<String, TomlToolLints>;
35133526

35143527
pub type TomlToolLints = BTreeMap<String, TomlLint>;
35153528

3516-
#[derive(Serialize, Deserialize, Debug, Clone)]
3529+
#[derive(Serialize, Debug, Clone)]
35173530
#[serde(untagged)]
35183531
pub enum TomlLint {
35193532
Level(TomlLintLevel),
35203533
Config(TomlLintConfig),
35213534
}
35223535

3536+
impl<'de> Deserialize<'de> for TomlLint {
3537+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3538+
where
3539+
D: de::Deserializer<'de>,
3540+
{
3541+
UntaggedEnumVisitor::new()
3542+
.string(|string| {
3543+
TomlLintLevel::deserialize(string.into_deserializer()).map(TomlLint::Level)
3544+
})
3545+
.map(|map| map.deserialize().map(TomlLint::Config))
3546+
.deserialize(deserializer)
3547+
}
3548+
}
3549+
35233550
impl TomlLint {
35243551
fn level(&self) -> TomlLintLevel {
35253552
match self {

tests/testsuite/bad_config.rs

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ Caused by:
13921392
|
13931393
6 | build = 3
13941394
| ^
1395-
expected a boolean or a string
1395+
invalid type: integer `3`, expected a boolean or string
13961396
",
13971397
)
13981398
.run();
@@ -1420,6 +1420,117 @@ fn warn_semver_metadata() {
14201420
.run();
14211421
}
14221422

1423+
#[cargo_test]
1424+
fn bad_http_ssl_version() {
1425+
// Invalid type in SslVersionConfig.
1426+
let p = project()
1427+
.file(
1428+
".cargo/config.toml",
1429+
r#"
1430+
[http]
1431+
ssl-version = ["tlsv1.2", "tlsv1.3"]
1432+
"#,
1433+
)
1434+
.file("src/lib.rs", "")
1435+
.build();
1436+
1437+
p.cargo("check")
1438+
.with_status(101)
1439+
.with_stderr(
1440+
"\
1441+
[ERROR] error in [..]/config.toml: could not load config key `http.ssl-version`
1442+
1443+
Caused by:
1444+
invalid type: sequence, expected a string or map
1445+
",
1446+
)
1447+
.run();
1448+
}
1449+
1450+
#[cargo_test]
1451+
fn bad_http_ssl_version_range() {
1452+
// Invalid type in SslVersionConfigRange.
1453+
let p = project()
1454+
.file(
1455+
".cargo/config.toml",
1456+
r#"
1457+
[http]
1458+
ssl-version.min = false
1459+
"#,
1460+
)
1461+
.file("src/lib.rs", "")
1462+
.build();
1463+
1464+
p.cargo("check")
1465+
.with_status(101)
1466+
.with_stderr(
1467+
"\
1468+
[ERROR] error in [..]/config.toml: could not load config key `http.ssl-version`
1469+
1470+
Caused by:
1471+
error in [..]/config.toml: `http.ssl-version.min` expected a string, but found a boolean
1472+
",
1473+
)
1474+
.run();
1475+
}
1476+
1477+
#[cargo_test]
1478+
fn bad_build_jobs() {
1479+
// Invalid type in JobsConfig.
1480+
let p = project()
1481+
.file(
1482+
".cargo/config.toml",
1483+
r#"
1484+
[build]
1485+
jobs = { default = true }
1486+
"#,
1487+
)
1488+
.file("src/lib.rs", "")
1489+
.build();
1490+
1491+
p.cargo("check")
1492+
.with_status(101)
1493+
.with_stderr(
1494+
"\
1495+
[ERROR] error in [..]/config.toml: could not load config key `build.jobs`
1496+
1497+
Caused by:
1498+
invalid type: map, expected an integer or string
1499+
",
1500+
)
1501+
.run();
1502+
}
1503+
1504+
#[cargo_test]
1505+
fn bad_build_target() {
1506+
// Invalid type in BuildTargetConfig.
1507+
let p = project()
1508+
.file(
1509+
".cargo/config.toml",
1510+
r#"
1511+
[build]
1512+
target.'cfg(unix)' = "x86_64"
1513+
"#,
1514+
)
1515+
.file("src/lib.rs", "")
1516+
.build();
1517+
1518+
p.cargo("check")
1519+
.with_status(101)
1520+
.with_stderr(
1521+
"\
1522+
[ERROR] error in [..]/config.toml: could not load config key `build.target`
1523+
1524+
Caused by:
1525+
error in [..]/config.toml: could not load config key `build.target`
1526+
1527+
Caused by:
1528+
invalid type: map, expected a string or array
1529+
",
1530+
)
1531+
.run();
1532+
}
1533+
14231534
#[cargo_test]
14241535
fn bad_target_cfg() {
14251536
// Invalid type in a StringList.

0 commit comments

Comments
 (0)