rust-lang / rust-lang/rust-clippy

Feature request: Comprehensive check for conflicting map keys in Serde

Open
#2,548 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-lint
Dominant language
Rust
Stars
13.5k
Forks
2.2k
Avg merge
2d 10h
Merged PRs (30d)
32

Description

This feature request is based on this issue: https://github.com/serde-rs/serde/issues/1181

Long story short: since proc-macro-derive runs on one type at a time, and it's a purely syntactic process without type checking information, serde_derive itself can't check for certain sneaky serialization errors that result in duplicate map keys. Example:

  • Suppose an internally-tagged enum has a newtype variant which itself contains an internally-tagged enum, either…
    • directly, or
    • indirectly, by means of being nested in 1 or more levels of untagged enums with a newtype variant.
  • Now:
    • if the containing and contained internally-tagged enums have the same type tag, or
    • if a struct variant of the contained enum has a field with the same name as the type tag,

then they end up being serialized at the same level, generating a conflict.

See the following MVCE:

#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate serde;

// internally-tagged top-level enum
#[derive(Serialize)]
#[serde(tag = "same")]
enum Foo {
    Qux(Z), // newtype variant around another internally-tagged enum itself
    Bar(X), // newtype variant around two levels of untagged, then one internally-tagged enum
    New(E), // newtype variant containing an untagged enum containing a struct variant with a conflicting field name
}

#[derive(Serialize)]
#[serde(untagged)]
enum X {
    Y(Y)
}

#[derive(Serialize)]
#[serde(untagged)]
enum Y {
    Z(Z),
}

#[derive(Serialize)]
#[serde(tag = "same")]
enum Z {
    Z,
}

#[derive(Serialize)]
#[serde(untagged)]
enum E {
    F(F),
}

#[derive(Serialize)]
#[serde(tag = "different")]
enum F {
    Struct { same: () },
}

fn main() {
    println!("{}", serde_json::to_string_pretty(&Foo::Bar(X::Y(Y::Z(Z::Z)))).unwrap());
    println!("");
    println!("{}", serde_json::to_string_pretty(&Foo::Qux(Z::Z)).unwrap());
    println!("");
    println!("{}", serde_json::to_string_pretty(&Foo::New(E::F(F::Struct { same: () }))).unwrap());
}

Output:

{
  "same": "Bar",
  "same": "Z"
}

{
  "same": "Qux",
  "same": "Z"
}

{
  "same": "New",
  "different": "Struct",
  "same": null
}

Checking for these conflicts requires traversing the transitive closure of the contained variants and types, which I think necessitates proper type checking. I will try and dive deep into Clippy and start implementing a lint pass that realizes this check. All feedback and suggestions are welcome.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the linked Serde issue and the MVCE in this issue, then investigate Clippy's lint-pass infrastructure and how it obtains type information. Done means detecting the described transitive duplicate-map-key cases and reporting them as a Clippy lint, with coverage for the direct, nested untagged, and conflicting-field examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.