diff --git a/src/serde_tokenstream.rs b/src/serde_tokenstream.rs index 9a88c11..b1377c4 100644 --- a/src/serde_tokenstream.rs +++ b/src/serde_tokenstream.rs @@ -271,6 +271,20 @@ fn flatten_none_groups(stream: TokenStream) -> TokenStream { .collect() } +/// Returns true if this token tree is an empty `macro_rules!` substitution. +fn is_empty_substitution(tt: &TokenTree) -> bool { + match tt { + TokenTree::Group(group) if group.delimiter() == Delimiter::None => { + // Recursively check to deal with nested macro invocations. + group.stream().into_iter().all(|tt| is_empty_substitution(&tt)) + } + TokenTree::Group(_) + | TokenTree::Ident(_) + | TokenTree::Punct(_) + | TokenTree::Literal(_) => false, + } +} + type InternalResult = std::result::Result; struct TokenDe { @@ -307,7 +321,24 @@ impl TokenDe { } } + // Skip over empty macro_rules substitutions. + // + // Taking care of them here means that other parts of the code don't need to + // worry about them -- so `next` and `peek` never return one, and + // `last` and `current` never point at one. + fn skip_empty_substitutions(&mut self) { + while self.input.peek().is_some_and(is_empty_substitution) { + self.input.next(); + } + } + + fn peek(&mut self) -> Option<&TokenTree> { + self.skip_empty_substitutions(); + self.input.peek() + } + fn next(&mut self) -> Option { + self.skip_empty_substitutions(); let next = self.input.next(); self.last = @@ -348,12 +379,66 @@ impl TokenDe { } } + /// Consumes the next token if it is a Delimiter::None group. + /// + /// These groups occur with declarative macro substitutions. + /// + /// Every `deserialize_*` method that reads a token (other than + /// `deserialize_bytes` which is special) starts by checking this. + fn take_transparent(&mut self) -> Option { + let group = match self.peek() { + Some(TokenTree::Group(group)) + if group.delimiter() == Delimiter::None => + { + group.clone() + } + Some(TokenTree::Group(_)) + | Some(TokenTree::Ident(_)) + | Some(TokenTree::Punct(_)) + | Some(TokenTree::Literal(_)) + | None => return None, + }; + self.next().map(|_| group) + } + + /// Deserializes the value obtained from `take_transparent`. + fn deserialize_transparent( + group: &Group, + what: &str, + deserialize: F, + ) -> InternalResult + where + F: FnOnce(&mut TokenDe) -> InternalResult, + { + // Flatten nested groups from recursive macro_rules. + let stream = flatten_none_groups(group.stream()); + + let mut inner = TokenDe::new(group, &stream); + let value = deserialize(&mut inner)?; + + match inner.next() { + None => Ok(value), + Some(_) => Err(InternalError::Spanned(spanned_error( + group, + format!("expected only {}, but found `{}`", what, group), + ))), + } + } + fn deserialize_int(&mut self, visit: F) -> InternalResult where F: FnOnce(T) -> InternalResult, T: std::str::FromStr, T::Err: Display, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + type_name::(), + |inner| inner.deserialize_int(visit), + ); + } + let next = self.next(); let mut stream = Vec::new(); @@ -387,6 +472,14 @@ impl TokenDe { T: std::str::FromStr, T::Err: Display, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + type_name::(), + |inner| inner.deserialize_float(visit), + ); + } + let next = self.next(); let mut stream = Vec::new(); @@ -452,7 +545,7 @@ impl<'de> MapAccess<'de> for TokenDe { where K: serde::de::DeserializeSeed<'de>, { - let keytok = match self.input.peek() { + let keytok = match self.peek() { None => return Ok(None), Some(token) => token.clone(), }; @@ -494,7 +587,7 @@ impl<'de> MapAccess<'de> for TokenDe { where V: DeserializeSeed<'de>, { - let valtok = self.input.peek().cloned(); + let valtok = self.peek().cloned(); let value = seed.deserialize(&mut *self); // We've processed the expected member via seed.deserialize. @@ -525,7 +618,7 @@ impl<'de> SeqAccess<'de> for TokenDe { where T: DeserializeSeed<'de>, { - let eltok = match self.input.peek() { + let eltok = match self.peek() { None => return Ok(None), Some(token) => token.clone(), }; @@ -667,6 +760,12 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent(&group, "bool", |inner| { + inner.deserialize_bool(visitor) + }); + } + match self.next() { Some(TokenTree::Ident(ident)) if ident == "true" => { visitor.visit_bool(true) @@ -690,6 +789,14 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "a string", + |inner| inner.deserialize_string(visitor), + ); + } + let token = self.next(); let value = match &token { Some(TokenTree::Ident(ident)) => Some(ident.to_string()), @@ -719,6 +826,14 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "an array", + |inner| inner.deserialize_seq(visitor), + ); + } + let next = self.next(); if let Some(TokenTree::Group(group)) = &next { @@ -732,13 +847,21 @@ impl<'de> Deserializer<'de> for &mut TokenDe { fn deserialize_struct( self, - _name: &'static str, - _fields: &'static [&'static str], + name: &'static str, + fields: &'static [&'static str], visitor: V, ) -> InternalResult where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "a struct", + |inner| inner.deserialize_struct(name, fields, visitor), + ); + } + let next = self.next(); if let Some(TokenTree::Group(group)) = &next { @@ -758,6 +881,14 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "a map", + |inner| inner.deserialize_map(visitor), + ); + } + let next = self.next(); if let Some(TokenTree::Group(group)) = &next { @@ -771,13 +902,47 @@ impl<'de> Deserializer<'de> for &mut TokenDe { fn deserialize_enum( self, - _name: &'static str, - _variants: &'static [&'static str], + name: &'static str, + variants: &'static [&'static str], visitor: V, ) -> InternalResult where V: Visitor<'de>, { + // Do the transparent business here rather than in `deserialize_identifier`. + // + // For example, with an enum `Kind { A, B(u32) }` and a declarative + // macro that forwards an expression into the attribute: + // + // macro_rules! wrap { + // ($k:expr) => { + // #[annotation { kind = $k }] + // fn test() {} + // }; + // } + // + // wrap!(B(4)); + // + // the value corresponding to `kind` is a single `Delimiter::None` group + // containing two token trees, `B` and `(4)`: + // + // kind = ⟨B (4)⟩ + // + // When deserializing an enum, we would read the variant name via + // `deserialize_identifier`, and then the payload via + // `newtype_variant_seed`. Unwrapping the group here lets both reads + // happen over here, so the trailing-token check sees nothing left over. + // If the group were unwrapped in `deserialize_identifier` instead, only + // `B` would be read from inside it and `(4)` would be rejected as a + // stray token. + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + &format!("a variant of `{}`", name), + |inner| inner.deserialize_enum(name, variants, visitor), + ); + } + visitor.visit_enum(self) } @@ -785,6 +950,14 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "an identifier", + |inner| inner.deserialize_identifier(visitor), + ); + } + let next = self.next(); if let Some(ident @ TokenTree::Ident(_)) = next { @@ -798,6 +971,14 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "a char", + |inner| inner.deserialize_char(visitor), + ); + } + let next = self.next(); if let Some(tt) = &next { @@ -815,6 +996,14 @@ impl<'de> Deserializer<'de> for &mut TokenDe { where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "a unit", + |inner| inner.deserialize_unit(visitor), + ); + } + let next = self.next(); if let Some(TokenTree::Group(group)) = &next { @@ -830,12 +1019,20 @@ impl<'de> Deserializer<'de> for &mut TokenDe { fn deserialize_tuple( self, - _len: usize, + len: usize, visitor: V, ) -> InternalResult where V: Visitor<'de>, { + if let Some(group) = self.take_transparent() { + return TokenDe::deserialize_transparent( + &group, + "a tuple", + |inner| inner.deserialize_tuple(len, visitor), + ); + } + let next = self.next(); if let Some(TokenTree::Group(group)) = &next { @@ -872,8 +1069,11 @@ impl<'de> Deserializer<'de> for &mut TokenDe { } // A None delimiter occurs for a macro_rules! substitution. We // can simply descend into those tokens. - Delimiter::None => TokenDe::new(group, &group.stream()) - .deserialize_any(visitor), + Delimiter::None => TokenDe::deserialize_transparent( + group, + "a value", + |inner| inner.deserialize_any(visitor), + ), }, Some(TokenTree::Ident(ident)) if *ident == "true" => { visitor.visit_bool(true) @@ -1099,7 +1299,7 @@ impl<'de> Deserializer<'de> for &mut TokenDe { loop { tokens.push(token); - token = match self.input.peek() { + token = match self.peek() { None => break, Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => { break; @@ -1945,4 +2145,174 @@ mod tests { assert_eq!(t.c, 9223372036854775808); assert_eq!(t.d, 170141183460469231731687303715884105727); } + + /// Builds a Delimiter::None group, emulating what rustc does for + /// macro_rules substitutions. + fn none_group(inner: TokenStream) -> TokenTree { + TokenTree::Group(Group::new(Delimiter::None, inner)) + } + + #[test] + fn test_none_group_nested() { + #[derive(Deserialize, Debug)] + struct Test { + s: String, + } + + let nested = |inner: TokenStream| { + none_group(TokenStream::from(none_group(inner))) + }; + + let mut tokens = quote! { s = }; + tokens.extend([nested(quote! { hello })]); + assert_eq!(from_tokenstream::(&tokens).unwrap().s, "hello"); + + let mut tokens = quote! { s = }; + tokens.extend([nested(quote! {})]); + assert_eq!( + from_tokenstream::(&tokens).unwrap_err().to_string(), + "expected a string following `=`" + ); + + let mut tokens = quote! { s = }; + tokens.extend([nested(quote! { foo::bar })]); + assert_eq!( + from_tokenstream::(&tokens).unwrap_err().to_string(), + "expected only a string, but found `foo :: bar`" + ); + } + + #[test] + fn test_none_group_any() { + #[derive(Deserialize, Debug, PartialEq)] + #[serde(untagged)] + enum Value { + S(String), + N(u32), + } + + #[derive(Deserialize, Debug)] + struct Test { + v: Value, + } + + // A single value is transparent. + let mut tokens = quote! { v = }; + tokens.extend([none_group(quote! { hello })]); + assert_eq!( + from_tokenstream::(&tokens).unwrap().v, + Value::S("hello".to_string()) + ); + let mut tokens = quote! { v = }; + tokens.extend([none_group(quote! { 5 })]); + assert_eq!(from_tokenstream::(&tokens).unwrap().v, Value::N(5)); + + // An empty group is a missing value. + let mut tokens = quote! { v = }; + tokens.extend([none_group(quote! {})]); + assert_eq!( + from_tokenstream::(&tokens).unwrap_err().to_string(), + "expected a value following `=`" + ); + + // More than one value is an error (don't just truncate it to the + // first!) + let mut tokens = quote! { v = }; + tokens.extend([none_group(quote! { foo::bar })]); + assert_eq!( + from_tokenstream::(&tokens).unwrap_err().to_string(), + "expected only a value, but found `foo :: bar`" + ); + } + + #[test] + fn test_none_group_kinds() { + #[derive(Deserialize, Debug, PartialEq)] + struct Inner { + x: u32, + } + + #[derive(Deserialize, Debug, PartialEq)] + enum Kind { + A, + B(u32), + } + + #[derive(Deserialize, Debug, PartialEq)] + struct Test { + b: bool, + n: i64, + f: f32, + c: char, + u: (), + t: (u32, bool), + v: Vec, + s: Inner, + k: Kind, + m: std::collections::BTreeMap, + o: Option, + } + + let mut tokens = TokenStream::new(); + for (key, value) in [ + ("b", quote! { true }), + ("n", quote! { -5 }), + ("f", quote! { 1.5 }), + ("c", quote! { 'c' }), + ("u", quote! { () }), + ("t", quote! { (1, false) }), + ("v", quote! { [1, 2] }), + ("s", quote! { { x = 3 } }), + ("k", quote! { B(4) }), + ("m", quote! { { a = 1 } }), + ("o", quote! { "some" }), + ] { + let key = + proc_macro2::Ident::new(key, proc_macro2::Span::call_site()); + tokens + .extend([none_group(TokenStream::from(TokenTree::Ident(key)))]); + tokens.extend(quote! { = }); + tokens.extend([none_group(value)]); + tokens.extend(quote! { , }); + } + let test = from_tokenstream::(&tokens).unwrap(); + assert_eq!( + test, + Test { + b: true, + n: -5, + f: 1.5, + c: 'c', + u: (), + t: (1, false), + v: vec![1, 2], + s: Inner { x: 3 }, + k: Kind::B(4), + m: [("a".to_string(), 1)].into_iter().collect(), + o: Some("some".to_string()), + } + ); + + #[derive(Deserialize, Debug)] + struct Just { + k: Kind, + } + + let mut tokens = quote! { k = }; + tokens.extend([none_group(quote! { A })]); + assert_eq!(from_tokenstream::(&tokens).unwrap().k, Kind::A); + + let mut tokens = quote! { k = }; + tokens.extend([none_group(quote! {})]); + assert_eq!( + from_tokenstream::(&tokens).unwrap_err().to_string(), + "expected an identifier following `=`" + ); + let mut tokens = quote! { k = }; + tokens.extend([none_group(quote! { A B })]); + assert_eq!( + from_tokenstream::(&tokens).unwrap_err().to_string(), + "expected only a variant of `Kind`, but found `A B`" + ); + } } diff --git a/testlib/src/lib.rs b/testlib/src/lib.rs index dec6047..4669dc2 100644 --- a/testlib/src/lib.rs +++ b/testlib/src/lib.rs @@ -228,6 +228,24 @@ pub fn rejected( } } +// Fixture for TokenStreamWrapper values. +#[derive(Deserialize)] +#[allow(dead_code)] +struct Tokens { + tokens: serde_tokenstream::TokenStreamWrapper, +} + +#[proc_macro_attribute] +pub fn tokens( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + match from_tokenstream::(&attr.into()) { + Ok(_) => item, + Err(err) => err.to_compile_error().into(), + } +} + #[proc_macro_attribute] pub fn outer( _attr: proc_macro::TokenStream, diff --git a/ui-tests/tests/ui/bad_parse_wrapper_from_macro_rules_empty.rs b/ui-tests/tests/ui/bad_parse_wrapper_from_macro_rules_empty.rs new file mode 100644 index 0000000..20c23a6 --- /dev/null +++ b/ui-tests/tests/ui/bad_parse_wrapper_from_macro_rules_empty.rs @@ -0,0 +1,23 @@ +// Copyright 2026 Oxide Computer Company + +// An empty macro_rules substitution into a ParseWrapper must produce a +// missing-value error, similar to plain values. + +use testlib::annotation; + +macro_rules! wrap { + ($v:vis ,) => { + #[annotation { + string = "test", + options = OptionA, + unit = (), + tup = (1, 2.0), + bool_expr = $v, + }] + fn test() {} + }; +} + +wrap!(,); + +fn main() {} diff --git a/ui-tests/tests/ui/bad_parse_wrapper_from_macro_rules_empty.stderr b/ui-tests/tests/ui/bad_parse_wrapper_from_macro_rules_empty.stderr new file mode 100644 index 0000000..d08bdb2 --- /dev/null +++ b/ui-tests/tests/ui/bad_parse_wrapper_from_macro_rules_empty.stderr @@ -0,0 +1,10 @@ +error: expected a value, but found `,` + --> tests/ui/bad_parse_wrapper_from_macro_rules_empty.rs:15:27 + | +15 | bool_expr = $v, + | ^ +... +21 | wrap!(,); + | -------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/bad_string_from_macro_rules_empty.rs b/ui-tests/tests/ui/bad_string_from_macro_rules_empty.rs new file mode 100644 index 0000000..0bdde49 --- /dev/null +++ b/ui-tests/tests/ui/bad_string_from_macro_rules_empty.rs @@ -0,0 +1,22 @@ +// Copyright 2026 Oxide Computer Company + +// An empty macro_rules substitution (such as an empty `vis`) must be a +// missing-value error, not a panic. + +use testlib::annotation; + +macro_rules! wrap { + ($v:vis ,) => { + #[annotation { + string = $v, + options = OptionA, + unit = (), + tup = (1, 2.0), + }] + fn test() {} + }; +} + +wrap!(,); + +fn main() {} diff --git a/ui-tests/tests/ui/bad_string_from_macro_rules_empty.stderr b/ui-tests/tests/ui/bad_string_from_macro_rules_empty.stderr new file mode 100644 index 0000000..9ad810c --- /dev/null +++ b/ui-tests/tests/ui/bad_string_from_macro_rules_empty.stderr @@ -0,0 +1,10 @@ +error: expected a string, but found `,` + --> tests/ui/bad_string_from_macro_rules_empty.rs:11:24 + | +11 | string = $v, + | ^ +... +20 | wrap!(,); + | -------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/bad_string_from_macro_rules_path.stderr b/ui-tests/tests/ui/bad_string_from_macro_rules_path.stderr index f201cf1..c6e55d8 100644 --- a/ui-tests/tests/ui/bad_string_from_macro_rules_path.stderr +++ b/ui-tests/tests/ui/bad_string_from_macro_rules_path.stderr @@ -1,4 +1,4 @@ -error: expected a string, but found `foo::bar` +error: expected only a string, but found `foo::bar` --> tests/ui/bad_string_from_macro_rules_path.rs:20:7 | 20 | wrap!(foo::bar); diff --git a/ui-tests/tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.rs b/ui-tests/tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.rs new file mode 100644 index 0000000..c259ac7 --- /dev/null +++ b/ui-tests/tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.rs @@ -0,0 +1,19 @@ +// Copyright 2026 Oxide Computer Company + +// An empty macro_rules substitution into a TokenStreamWrapper must produce a +// missing-value error. + +use testlib::tokens; + +macro_rules! wrap { + ($v:vis ,) => { + #[tokens { + tokens = $v, + }] + fn test() {} + }; +} + +wrap!(,); + +fn main() {} diff --git a/ui-tests/tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.stderr b/ui-tests/tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.stderr new file mode 100644 index 0000000..701e18c --- /dev/null +++ b/ui-tests/tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.stderr @@ -0,0 +1,10 @@ +error: expected a value, but found `,` + --> tests/ui/bad_token_stream_wrapper_from_macro_rules_empty.rs:11:24 + | +11 | tokens = $v, + | ^ +... +17 | wrap!(,); + | -------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/parse_wrapper_from_macro_rules_empty.rs b/ui-tests/tests/ui/parse_wrapper_from_macro_rules_empty.rs new file mode 100644 index 0000000..d20df5e --- /dev/null +++ b/ui-tests/tests/ui/parse_wrapper_from_macro_rules_empty.rs @@ -0,0 +1,24 @@ +// Copyright 2026 Oxide Computer Company + +// Empty macro_rules substitutions around a ParseWrapper value must be skipped. +// (The error produced must be for the later `maybe_string` field.) + +use testlib::annotation; + +macro_rules! wrap { + ($v:vis ,) => { + #[annotation { + string = "test", + options = OptionA, + unit = (), + tup = (1, 2.0), + bool_expr = $v true $v, + maybe_string = 1, + }] + fn test() {} + }; +} + +wrap!(,); + +fn main() {} diff --git a/ui-tests/tests/ui/parse_wrapper_from_macro_rules_empty.stderr b/ui-tests/tests/ui/parse_wrapper_from_macro_rules_empty.stderr new file mode 100644 index 0000000..decdea2 --- /dev/null +++ b/ui-tests/tests/ui/parse_wrapper_from_macro_rules_empty.stderr @@ -0,0 +1,10 @@ +error: expected a string, but found `1` + --> tests/ui/parse_wrapper_from_macro_rules_empty.rs:16:28 + | +16 | maybe_string = 1, + | ^ +... +22 | wrap!(,); + | -------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/string_from_macro_rules.rs b/ui-tests/tests/ui/string_from_macro_rules.rs new file mode 100644 index 0000000..9e3bb4f --- /dev/null +++ b/ui-tests/tests/ui/string_from_macro_rules.rs @@ -0,0 +1,23 @@ +// Copyright 2026 Oxide Computer Company + +// With macro_rules substitutions, rustc will wrap the substitutions inside a +// `Delimiter::None` group. Ensure that such groups are accepted. (In this +// test, the error produced must be for the later `unit` field.) + +use testlib::annotation; + +macro_rules! wrap { + ($s:expr) => { + #[annotation { + string = $s, + options = OptionA, + unit = 1, + tup = (1, 2.0), + }] + fn test() {} + }; +} + +wrap!(foo); + +fn main() {} diff --git a/ui-tests/tests/ui/string_from_macro_rules.stderr b/ui-tests/tests/ui/string_from_macro_rules.stderr new file mode 100644 index 0000000..a0cbe07 --- /dev/null +++ b/ui-tests/tests/ui/string_from_macro_rules.stderr @@ -0,0 +1,10 @@ +error: expected a unit, but found `1` + --> tests/ui/string_from_macro_rules.rs:14:20 + | +14 | unit = 1, + | ^ +... +21 | wrap!(foo); + | ---------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/string_from_macro_rules_empty_prefix.rs b/ui-tests/tests/ui/string_from_macro_rules_empty_prefix.rs new file mode 100644 index 0000000..5448e54 --- /dev/null +++ b/ui-tests/tests/ui/string_from_macro_rules_empty_prefix.rs @@ -0,0 +1,23 @@ +// Copyright 2026 Oxide Computer Company + +// An empty macro_rules substitution before a value must be skipped. (The error +// produced must be for the later `maybe_string` field.) + +use testlib::annotation; + +macro_rules! wrap { + ($v:vis ,) => { + #[annotation { + string = $v foo, + options = OptionA, + unit = (), + tup = (1, 2.0), + maybe_string = 1, + }] + fn test() {} + }; +} + +wrap!(,); + +fn main() {} diff --git a/ui-tests/tests/ui/string_from_macro_rules_empty_prefix.stderr b/ui-tests/tests/ui/string_from_macro_rules_empty_prefix.stderr new file mode 100644 index 0000000..8e84ed2 --- /dev/null +++ b/ui-tests/tests/ui/string_from_macro_rules_empty_prefix.stderr @@ -0,0 +1,10 @@ +error: expected a string, but found `1` + --> tests/ui/string_from_macro_rules_empty_prefix.rs:15:28 + | +15 | maybe_string = 1, + | ^ +... +21 | wrap!(,); + | -------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/string_from_macro_rules_empty_suffix.rs b/ui-tests/tests/ui/string_from_macro_rules_empty_suffix.rs new file mode 100644 index 0000000..9c6d8b8 --- /dev/null +++ b/ui-tests/tests/ui/string_from_macro_rules_empty_suffix.rs @@ -0,0 +1,23 @@ +// Copyright 2026 Oxide Computer Company + +// An macro_rules substitution after a value must be skipped. (The error +// produced must be for the later `maybe_string` field.) + +use testlib::annotation; + +macro_rules! wrap { + ($v:vis ,) => { + #[annotation { + string = foo $v, + options = OptionA, + unit = (), + tup = (1, 2.0), + maybe_string = 1, + }] + fn test() {} + }; +} + +wrap!(,); + +fn main() {} diff --git a/ui-tests/tests/ui/string_from_macro_rules_empty_suffix.stderr b/ui-tests/tests/ui/string_from_macro_rules_empty_suffix.stderr new file mode 100644 index 0000000..04bdb06 --- /dev/null +++ b/ui-tests/tests/ui/string_from_macro_rules_empty_suffix.stderr @@ -0,0 +1,10 @@ +error: expected a string, but found `1` + --> tests/ui/string_from_macro_rules_empty_suffix.rs:15:28 + | +15 | maybe_string = 1, + | ^ +... +21 | wrap!(,); + | -------- in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/ui-tests/tests/ui/values_from_macro_rules.rs b/ui-tests/tests/ui/values_from_macro_rules.rs new file mode 100644 index 0000000..53c92ad --- /dev/null +++ b/ui-tests/tests/ui/values_from_macro_rules.rs @@ -0,0 +1,26 @@ +// Copyright 2026 Oxide Computer Company + +// Test that macro_rules substitutions are accepted for every value kind. (The +// error must be for the later `maybe_string` field.) + +use testlib::annotation; + +macro_rules! wrap { + ($o:expr, $u:expr, $t:expr, $m:expr, $n:expr, $g:expr) => { + #[annotation { + string = "test", + options = $o, + unit = $u, + tup = $t, + many = $m, + nested = { squeaker = "x", eyas = $n, gosling = 1.0 }, + painted = { color = { red = true, green = $g, blue = false } }, + maybe_string = 1, + }] + fn test() {} + }; +} + +wrap!(OptionA, (), (1, 2.0), ["a", b], 7, false); + +fn main() {} diff --git a/ui-tests/tests/ui/values_from_macro_rules.stderr b/ui-tests/tests/ui/values_from_macro_rules.stderr new file mode 100644 index 0000000..c200126 --- /dev/null +++ b/ui-tests/tests/ui/values_from_macro_rules.stderr @@ -0,0 +1,10 @@ +error: expected a string, but found `1` + --> tests/ui/values_from_macro_rules.rs:18:28 + | +18 | maybe_string = 1, + | ^ +... +24 | wrap!(OptionA, (), (1, 2.0), ["a", b], 7, false); + | ------------------------------------------------ in this macro invocation + | + = note: this error originates in the macro `wrap` (in Nightly builds, run with -Z macro-backtrace for more info)