Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions src/ibidem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,22 @@ use crate::serde_tokenstream::spanned_error;
/// generates code where the caller of that macro might want to augment the
/// generated code.
///
/// # Limitations
///
/// This type can only be deserialized within [`from_tokenstream`] or
/// [`from_tokenstream_spanned`], and only in positions where serde does not
/// perform internal buffering (e.g., it cannot be used inside
/// `#[serde(flatten)]` or `#[serde(untagged)]`). When used with internal
/// buffering, this produces an error.
///
/// # Panics
///
/// The [`Deserialize`] implementation for `TokenStreamWrapper` will panic if
/// it is not used in the context of [`from_tokenstream`].
/// it is not used in the context of [`from_tokenstream`] or
/// [`from_tokenstream_spanned`].
///
/// [`from_tokenstream`]: crate::from_tokenstream
/// [`from_tokenstream_spanned`]: crate::from_tokenstream_spanned
#[derive(Clone, Debug, Default)]
pub struct TokenStreamWrapper(TokenStream);

Expand Down Expand Up @@ -59,13 +69,23 @@ impl std::ops::Deref for TokenStreamWrapper {
/// This extends [`TokenStreamWrapper`] by further interpreting the TokenStream
/// and guiding the user in the case of parse errors.
///
/// # Limitations
///
/// This type can only be deserialized within [`from_tokenstream`] or
/// [`from_tokenstream_spanned`], and only in positions where serde does not
/// perform internal buffering (e.g., it cannot be used inside
/// `#[serde(flatten)]` or `#[serde(untagged)]`). When used with internal
/// buffering, this produces an error.
///
/// # Panics
///
/// The [`Deserialize`] implementation for [`TokenStreamWrapper`] will panic if
/// it is not used in the context of [`from_tokenstream`].
/// The [`Deserialize`] implementation for `ParseWrapper` will panic if it is
/// not used in the context of [`from_tokenstream`] or
/// [`from_tokenstream_spanned`].
///
/// [`Parse`]: syn::parse::Parse
/// [`from_tokenstream`]: crate::from_tokenstream
/// [`from_tokenstream_spanned`]: crate::from_tokenstream_spanned
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)]
pub struct ParseWrapper<P: syn::parse::Parse>(P);

Expand Down Expand Up @@ -158,7 +178,16 @@ impl Visitor<'_> for WrapperVisitor {
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter.write_str("TokenStream")
// Serde shows this text to macro users in case of a wrapper being used
// with internal buffering. (Not for untagged, though, unfortunately,
// because serde swallows errors from untagged variants. Why does
// untagged exist at all if the UX is so bad? Great question, and the
// answer will be a mystery.)
formatter.write_str(
"a ParseWrapper or TokenStreamWrapper value; these cannot be used \
inside `#[serde(flatten)]`, `#[serde(untagged)]`, or similar -- \
this is a bug in the macro",
)
}

fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
Expand Down
55 changes: 55 additions & 0 deletions src/serde_tokenstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,61 @@ mod tests {
assert_eq!(wrapper.into_inner().to_string(), "x");
}

#[test]
fn test_parse_wrapper_buffered() {
// In situations with internal buffering, we must produce an error
// rather than panic.
#[derive(Deserialize)]
#[serde(untagged)]
#[allow(dead_code)]
enum Untagged {
I(ParseWrapper<syn::Ident>),
N(u32),
}

#[derive(Deserialize)]
struct Test {
#[allow(dead_code)]
u: Untagged,
}

match from_tokenstream::<Test>(&quote! { u = s }) {
// With untagged, the error produced by us gets swallowed.
Err(err) => assert_eq!(
err.to_string(),
"data did not match any variant of untagged enum Untagged"
),
Ok(_) => panic!("unexpected success"),
}

// With flatten, we can produce a helpful message.
#[derive(Deserialize)]
struct Inner {
#[allow(dead_code)]
id: ParseWrapper<syn::Ident>,
}

#[derive(Deserialize)]
struct Flat {
#[allow(dead_code)]
n: u32,
#[serde(flatten)]
#[allow(dead_code)]
inner: Inner,
}

match from_tokenstream::<Flat>(&quote! { n = 1, id = s }) {
Err(err) => assert_eq!(
err.to_string(),
"invalid type: string \"s\", expected a ParseWrapper or \
TokenStreamWrapper value; these cannot be used inside \
`#[serde(flatten)]`, `#[serde(untagged)]`, or similar -- \
this is a bug in the macro"
),
Ok(_) => panic!("unexpected success"),
}
}

#[test]
fn parse_u128() {
#[derive(Deserialize)]
Expand Down
26 changes: 26 additions & 0 deletions testlib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,29 @@ pub fn newtype_variant(
Err(err) => err.to_compile_error().into(),
}
}

// Tests the error in case of a ParseWrapper inside a #[serde(flatten)] struct.
#[derive(Deserialize)]
#[allow(dead_code)]
struct FlattenedWrapper {
n: u32,
#[serde(flatten)]
inner: FlattenedWrapperInner,
}

#[derive(Deserialize)]
#[allow(dead_code)]
struct FlattenedWrapperInner {
id: ParseWrapper<syn::Ident>,
}

#[proc_macro_attribute]
pub fn flattened_wrapper(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
match from_tokenstream::<FlattenedWrapper>(&attr.into()) {
Ok(_) => item,
Err(err) => err.to_compile_error().into(),
}
}
14 changes: 14 additions & 0 deletions ui-tests/tests/ui/bad_flatten_wrapper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2026 Oxide Computer Company

// Ensure that a ParseWrapper inside a `#[serde(flatten)]` struct produces a
// helpful error message.

use testlib::flattened_wrapper;

#[flattened_wrapper {
n = 1,
id = foo,
}]
fn test() {}

fn main() {}
10 changes: 10 additions & 0 deletions ui-tests/tests/ui/bad_flatten_wrapper.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
error: invalid type: string "foo", expected a ParseWrapper or TokenStreamWrapper value; these cannot be used inside `#[serde(flatten)]`, `#[serde(untagged)]`, or similar -- this is a bug in the macro
--> tests/ui/bad_flatten_wrapper.rs:8:1
|
8 | / #[flattened_wrapper {
9 | | n = 1,
10 | | id = foo,
11 | | }]
| |__^
|
= note: this error originates in the attribute macro `flattened_wrapper` (in Nightly builds, run with -Z macro-backtrace for more info)
Loading