1use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::{ToTokens, format_ident, quote, quote_spanned};
12use syn::{
13 Attribute, Data, DeriveInput, Error, Expr, ExprLit, Fields, Ident, Lit, LitStr, Meta,
14 MetaNameValue, Token, Type, parse::Parser, parse_macro_input, punctuated::Punctuated,
15};
16
17struct Deprecation(Option<String>);
24
25struct Docs(Option<String>);
28
29struct MessageField {
34 deprecated: Deprecation,
35 doc: Docs,
36 ident: Ident,
37 meta: MetaPairs,
38 ty: Type,
39}
40
41#[derive(Default)]
45struct MetaPairs(Vec<(String, String)>);
46
47#[derive(Default)]
51struct Tags(Vec<String>);
52
53impl Deprecation {
54 fn harvest(attrs: &[Attribute]) -> Self {
58 let reason = |a: &Attribute| {
59 let reason = match &a.meta {
60 Meta::Path(_) => String::new(),
61 Meta::NameValue(nv) => match &nv.value {
62 Expr::Lit(ExprLit {
63 lit: Lit::Str(s), ..
64 }) => s.value(),
65 _ => String::new(),
66 },
67 Meta::List(_) => {
68 let mut note = String::new();
69 let _ = a.parse_nested_meta(|m| {
70 let s: LitStr = m.value()?.parse()?;
71 if m.path.is_ident("note") {
72 note = s.value();
73 }
74 Ok(())
75 });
76 note
77 }
78 };
79
80 if reason.is_empty() {
81 "true".to_string()
82 } else {
83 reason
84 }
85 };
86
87 Deprecation(
88 attrs
89 .iter()
90 .find(|a| a.path().is_ident("deprecated"))
91 .map(reason),
92 )
93 }
94}
95
96impl Docs {
97 fn harvest(attrs: &[Attribute]) -> Self {
99 let lines: Vec<String> = attrs
100 .iter()
101 .filter(|a| a.path().is_ident("doc"))
102 .filter_map(|a| match &a.meta {
103 Meta::NameValue(nv) => match &nv.value {
104 Expr::Lit(ExprLit {
105 lit: Lit::Str(s), ..
106 }) => Some(s.value().trim().to_string()),
107 _ => None,
108 },
109 _ => None,
110 })
111 .collect();
112
113 Docs((!lines.is_empty()).then(|| lines.join("\n")))
114 }
115}
116
117impl MessageField {
118 fn ambient_inner(&self) -> Option<&Type> {
122 let Type::Path(tp) = &self.ty else {
123 return None;
124 };
125
126 if tp.qself.is_some() {
127 return None;
128 }
129
130 let seg = tp.path.segments.last()?;
131
132 if seg.ident != "Option" {
133 return None;
134 }
135
136 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
137 return None;
138 };
139
140 if args.args.len() != 1 {
141 return None;
142 }
143
144 match args.args.first()? {
145 syn::GenericArgument::Type(t) => Some(t),
146 _ => None,
147 }
148 }
149
150 fn parse(field: &syn::Field) -> syn::Result<Self> {
151 let ident = field.ident.clone().expect("named field");
152
153 if ident == "message" {
156 return Err(Error::new_spanned(
157 &ident,
158 "a message field must not be named `message`; \
159 tracing reserves it for the event text",
160 ));
161 }
162
163 let mut meta = MetaPairs::default();
164
165 for a in &field.attrs {
166 if a.path().is_ident("field") {
167 let kvs =
168 a.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)?;
169
170 for kv in &kvs {
171 meta.push(kv)?;
172 }
173 }
174 }
175
176 Ok(MessageField {
177 deprecated: Deprecation::harvest(&field.attrs),
178 doc: Docs::harvest(&field.attrs),
179 ident,
180 meta,
181 ty: field.ty.clone(),
182 })
183 }
184}
185
186impl MetaPairs {
187 fn push(&mut self, kv: &MetaNameValue) -> syn::Result<()> {
191 let Some(key) = kv.path.get_ident() else {
192 return Err(Error::new_spanned(
193 &kv.path,
194 "metadata keys must be identifiers",
195 ));
196 };
197
198 if self.0.iter().any(|(k, _)| key == k) {
199 return Err(Error::new_spanned(
200 key,
201 format!("duplicate metadata key `{key}`"),
202 ));
203 }
204
205 let value = match &kv.value {
206 Expr::Lit(ExprLit {
207 lit: Lit::Str(s), ..
208 }) => s.value(),
209 Expr::Lit(ExprLit {
210 lit: Lit::Int(i), ..
211 }) => i.base10_digits().to_string(),
212 Expr::Lit(ExprLit {
213 lit: Lit::Float(f), ..
214 }) => f.base10_digits().to_string(),
215 Expr::Lit(ExprLit {
216 lit: Lit::Bool(b), ..
217 }) => b.value.to_string(),
218 other => {
219 return Err(Error::new_spanned(
220 other,
221 "metadata values must be literals (str/int/bool/float)",
222 ));
223 }
224 };
225
226 self.0.push((key.to_string(), value));
227
228 Ok(())
229 }
230}
231
232impl Tags {
233 fn parse(value: &Expr) -> syn::Result<Self> {
237 let Expr::Array(array) = value else {
238 return Err(Error::new_spanned(
239 value,
240 "`tags` must be an array of string literals, e.g. `tags = [\"analytics\"]`",
241 ));
242 };
243
244 let mut tags = Vec::with_capacity(array.elems.len());
245
246 for elem in &array.elems {
247 let Expr::Lit(ExprLit {
248 lit: Lit::Str(s), ..
249 }) = elem
250 else {
251 return Err(Error::new_spanned(
252 elem,
253 "each tag must be a string literal",
254 ));
255 };
256
257 let tag = s.value();
258
259 if tag.is_empty() {
260 return Err(Error::new_spanned(s, "a tag must not be empty"));
261 }
262
263 if tag != tag.to_lowercase() {
264 return Err(Error::new_spanned(
265 s,
266 format!("tags must be lowercase; use `{}`", tag.to_lowercase()),
267 ));
268 }
269
270 tags.push(tag);
271 }
272
273 tags.sort_unstable();
274 tags.dedup();
275
276 Ok(Tags(tags))
277 }
278}
279
280impl ToTokens for Deprecation {
281 fn to_tokens(&self, tokens: &mut TokenStream2) {
282 tokens.extend(option_str(&self.0));
283 }
284}
285
286impl ToTokens for Docs {
287 fn to_tokens(&self, tokens: &mut TokenStream2) {
288 tokens.extend(option_str(&self.0));
289 }
290}
291
292impl ToTokens for MessageField {
293 fn to_tokens(&self, tokens: &mut TokenStream2) {
294 let MessageField {
295 deprecated,
296 doc,
297 ident,
298 meta,
299 ty,
300 } = self;
301
302 tokens.extend(quote! {
303 ::tracing_wide::catalogue::FieldDescriptor {
304 deprecated: #deprecated,
305 doc: #doc,
306 meta: #meta,
307 name: ::core::stringify!(#ident),
308 r#type: ::core::stringify!(#ty),
309 }
310 });
311 }
312}
313
314impl ToTokens for MetaPairs {
315 fn to_tokens(&self, tokens: &mut TokenStream2) {
316 let keys = self.0.iter().map(|(k, _)| k);
317 let vals = self.0.iter().map(|(_, v)| v);
318 tokens.extend(quote! { &[ #( (#keys, #vals) ),* ] });
319 }
320}
321
322impl ToTokens for Tags {
323 fn to_tokens(&self, tokens: &mut TokenStream2) {
324 let tags = self.0.iter();
325
326 tokens.extend(quote! { &[ #( #tags ),* ] });
327 }
328}
329
330#[proc_macro]
338pub fn event(input: TokenStream) -> TokenStream {
339 let expr = parse_macro_input!(input as Expr);
340
341 quote! {{
342 #[allow(unused_mut)]
344 let mut __tracing_wide_msg = #expr;
345 ::tracing_wide::__private::MessageBehaviour::join_ambient(&mut __tracing_wide_msg);
346 ::tracing_wide::__private::MessageBehaviour::emit(&__tracing_wide_msg);
347 }}
348 .into()
349}
350
351fn expand_message(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
354 let mut input: DeriveInput = syn::parse2(item)?;
355 let name = input.ident.clone();
356
357 if !input.generics.params.is_empty() {
361 return Err(Error::new_spanned(
362 &input.generics,
363 "#[message] does not support generic parameters \
364 (a message must be a concrete `'static` type)",
365 ));
366 }
367
368 let mut msg: Option<String> = None;
369 let mut level: Option<String> = None;
370 let mut tags = Tags::default();
371 let mut msg_meta = MetaPairs::default();
372
373 let metas = Punctuated::<Meta, Token![,]>::parse_terminated.parse2(attr)?;
374
375 for m in metas {
376 match m {
377 Meta::Path(p) => {
378 return Err(Error::new_spanned(
379 &p,
380 "expected `key = value`; `#[message]` takes no bare flags \
381 (serialization is enabled by `#[derive(Serialize)]`)",
382 ));
383 }
384 Meta::NameValue(nv) if nv.path.is_ident("msg") => {
385 if let Expr::Lit(ExprLit {
386 lit: Lit::Str(s), ..
387 }) = &nv.value
388 {
389 msg = Some(s.value());
390 } else {
391 return Err(Error::new_spanned(
392 &nv.value,
393 "`msg` must be a string literal",
394 ));
395 }
396 }
397 Meta::NameValue(nv) if nv.path.is_ident("level") => {
398 let lvl = match &nv.value {
399 Expr::Path(p) if p.path.get_ident().is_some() => {
400 p.path.get_ident().unwrap().to_string()
401 }
402 Expr::Lit(ExprLit {
403 lit: Lit::Str(s), ..
404 }) => s.value(),
405 _ => {
406 return Err(Error::new_spanned(
407 &nv.value,
408 "`level` must be one of trace/debug/info/warn/error",
409 ));
410 }
411 };
412 level = Some(lvl);
413 }
414 Meta::NameValue(nv) if nv.path.is_ident("tags") => {
415 tags = Tags::parse(&nv.value)?;
416 }
417 Meta::NameValue(nv) => msg_meta.push(&nv)?,
418 Meta::List(l) => {
419 return Err(Error::new_spanned(
420 &l.path,
421 "expected `key = value` or a bare flag, not a list",
422 ));
423 }
424 }
425 }
426 let msg = msg.unwrap_or_else(|| name.to_string());
427
428 let msg_record = msg.replace('{', "{{").replace('}', "}}");
433
434 let msg_doc = Docs::harvest(&input.attrs);
435 let msg_deprecation = Deprecation::harvest(&input.attrs);
436
437 let allow_deprecated = if msg_deprecation.0.is_some() {
440 quote! { #[allow(deprecated)] }
441 } else {
442 quote! {}
443 };
444
445 let (level_const, level_macro) = match level.as_deref().unwrap_or("info") {
446 "trace" => (format_ident!("TRACE"), format_ident!("trace")),
447 "debug" => (format_ident!("DEBUG"), format_ident!("debug")),
448 "info" => (format_ident!("INFO"), format_ident!("info")),
449 "warn" => (format_ident!("WARN"), format_ident!("warn")),
450 "error" => (format_ident!("ERROR"), format_ident!("error")),
451 other => {
452 return Err(Error::new_spanned(
453 &name,
454 format!("unknown level `{other}` (expected trace/debug/info/warn/error)"),
455 ));
456 }
457 };
458
459 let fields = match &input.data {
460 Data::Struct(s) => match &s.fields {
461 Fields::Named(named) => named
462 .named
463 .iter()
464 .map(MessageField::parse)
465 .collect::<syn::Result<Vec<_>>>()?,
466 _ => {
467 return Err(Error::new_spanned(
468 &name,
469 "#[message] requires named fields",
470 ));
471 }
472 },
473 _ => {
474 return Err(Error::new_spanned(
475 &name,
476 "#[message] can only be applied to structs",
477 ));
478 }
479 };
480
481 if let Data::Struct(s) = &mut input.data
484 && let Fields::Named(named) = &mut s.fields
485 {
486 for f in &mut named.named {
487 f.attrs.retain(|a| !a.path().is_ident("field"));
488 }
489 }
490
491 let idents: Vec<&Ident> = fields.iter().map(|f| &f.ident).collect();
492
493 let types: Vec<&Type> = fields.iter().map(|f| &f.ty).collect();
494
495 let ambient = fields.iter().filter_map(|f| {
496 f.ambient_inner().map(|inner| {
497 let ident = &f.ident;
498 quote! { (#ident, #inner) }
499 })
500 });
501
502 let origin = quote_spanned! { name.span() =>
506 ::tracing_wide::Origin {
507 column: ::core::column!(),
508 file: ::core::file!(),
509 krate: ::core::env!("CARGO_PKG_NAME"),
510 line: ::core::line!(),
511 module: ::core::module_path!(),
512 }
513 };
514
515 Ok(quote! {
516 #input
517
518 #allow_deprecated
519 impl #name {
520 pub const LEVEL: ::tracing_wide::__private::tracing::Level =
522 ::tracing_wide::__private::tracing::Level::#level_const;
523
524 pub const MSG: &'static str = #msg;
526
527 pub const ORIGIN: ::tracing_wide::Origin = #origin;
529
530 pub const TAGS: &'static [&'static str] = #tags;
532 }
533
534 #allow_deprecated
535 impl ::tracing_wide::Message for #name {
536 fn as_any(&self) -> &dyn ::core::any::Any { self }
537 ::tracing_wide::__message_facet_method! {}
538 ::tracing_wide::__message_serialize_method! {}
539 fn level(&self) -> ::tracing_wide::__private::tracing::Level { Self::LEVEL }
540 fn msg(&self) -> &'static str { Self::MSG }
541 fn origin(&self) -> &'static ::tracing_wide::Origin { &Self::ORIGIN }
542 fn tags(&self) -> &'static [&'static str] { Self::TAGS }
543 }
544
545 #[doc(hidden)]
546 #allow_deprecated
547 impl ::tracing_wide::__private::Sealed for #name {}
548
549 ::tracing_wide::__register_message! {
550 ::tracing_wide::catalogue::MessageDescriptor {
551 deprecated: #msg_deprecation,
552 doc: #msg_doc,
553 fields: &[ #( #fields ),* ],
554 level: ::tracing_wide::catalogue::LevelName::#level_const,
555 meta: #msg_meta,
556 msg: #msg,
557 origin: #name::ORIGIN,
558 tags: #name::TAGS,
559 type_id: ::core::any::TypeId::of::<#name>(),
560 }
561 }
562
563 #[doc(hidden)]
564 #allow_deprecated
565 impl ::tracing_wide::__private::MessageBehaviour for #name {
566 ::tracing_wide::__message_ambient_method! {
567 #( #ambient ),*
568 }
569
570 #[allow(deprecated)]
573 fn record(&self) {
574 ::tracing_wide::__private::tracing::#level_macro!( #( #idents = &self.#idents, )* #msg_record );
575 }
576 }
577
578 const _: fn() = || {
579 fn __tracing_wide_assert_field<T: ::tracing_wide::Field>() {}
580 #( __tracing_wide_assert_field::<#types>(); )*
581 };
582 })
583}
584
585#[proc_macro_attribute]
597pub fn message(attr: TokenStream, item: TokenStream) -> TokenStream {
598 expand_message(attr.into(), item.into())
599 .unwrap_or_else(Error::into_compile_error)
600 .into()
601}
602
603fn option_str(value: &Option<String>) -> TokenStream2 {
606 match value {
607 Some(s) => quote! { ::core::option::Option::Some(#s) },
608 None => quote! { ::core::option::Option::None },
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use quote::quote;
616
617 #[test]
618 fn message_expansion_pretty_prints() {
619 let attr = quote! { msg = "hi", level = warn, owner = "x" };
620 let item = quote! {
621 struct Demo {
623 a: usize,
624 #[field(unit = "ms")]
625 b: usize,
626 }
627 };
628 let expanded = expand_message(attr, item).expect("expansion succeeds");
629 let file = syn::parse2::<syn::File>(expanded).expect("output is valid Rust");
630 let pretty = prettyplease::unparse(&file);
631
632 assert!(pretty.contains("impl ::tracing_wide::Message for Demo"));
633 assert!(pretty.contains("pub const MSG"));
634 println!("{pretty}");
635 }
636
637 #[test]
638 fn message_harvests_deprecation() {
639 let pretty = |item: TokenStream2| {
640 let expanded = expand_message(quote! { msg = "m" }, item).expect("expansion succeeds");
641 prettyplease::unparse(&syn::parse2::<syn::File>(expanded).unwrap())
642 };
643
644 let noted = pretty(quote! { #[deprecated = "use `n`"] struct M { a: usize } });
645 assert!(noted.contains(r#"Some("use `n`")"#), "{noted}");
646 assert!(noted.contains("#[allow(deprecated)]\nimpl"), "{noted}");
647
648 let bare = pretty(quote! { #[deprecated] struct M { a: usize } });
649 assert!(bare.contains(r#"Some("true")"#), "{bare}");
650
651 let meta =
652 pretty(quote! { #[deprecated(since = "0.2", note = "gone")] struct M { a: usize } });
653 assert!(meta.contains(r#"Some("gone")"#), "{meta}");
654
655 let field = pretty(quote! { struct M { #[deprecated = "old"] a: usize } });
656 assert!(field.contains(r#"Some("old")"#), "{field}");
657 assert!(!field.contains("#[allow(deprecated)]\nimpl"), "{field}");
658
659 let plain = pretty(quote! { struct M { a: usize } });
662 let flat: String = plain.chars().filter(|c| !c.is_whitespace()).collect();
663 assert!(
664 flat.contains("deprecated:::core::option::Option::None"),
665 "{plain}"
666 );
667 assert!(!plain.contains("#[allow(deprecated)]\nimpl"), "{plain}");
668 }
669
670 #[test]
671 fn message_rejects_non_literal_meta() {
672 let err = expand_message(
673 quote! { owner = some_path },
674 quote! { struct M { a: usize } },
675 )
676 .unwrap_err();
677 assert!(err.to_string().contains("must be literals"));
678 }
679
680 #[test]
681 fn message_rejects_bare_flag() {
682 let err =
683 expand_message(quote! { serialize }, quote! { struct M { a: usize } }).unwrap_err();
684 assert!(err.to_string().contains("takes no bare flags"));
685 }
686
687 #[test]
688 fn message_rejects_non_struct() {
689 let err = expand_message(quote! {}, quote! { enum E { A } }).unwrap_err();
690 assert!(err.to_string().contains("can only be applied to structs"));
691 }
692
693 #[test]
694 fn message_rejects_unnamed_fields() {
695 let err = expand_message(quote! {}, quote! { struct T(usize); }).unwrap_err();
696 assert!(err.to_string().contains("requires named fields"));
697 }
698
699 #[test]
700 fn message_rejects_meta_list() {
701 let err =
702 expand_message(quote! { owner(x) }, quote! { struct M { a: usize } }).unwrap_err();
703 assert!(err.to_string().contains("not a list"));
704 }
705
706 #[test]
707 fn message_rejects_duplicate_meta_key() {
708 let err = expand_message(
709 quote! { owner = "a", owner = "b" },
710 quote! { struct M { a: usize } },
711 )
712 .unwrap_err();
713 assert!(err.to_string().contains("duplicate metadata key `owner`"));
714
715 let err = expand_message(
716 quote! {},
717 quote! { struct M { #[field(unit = "ms", unit = "s")] a: usize } },
718 )
719 .unwrap_err();
720 assert!(err.to_string().contains("duplicate metadata key `unit`"));
721 }
722
723 #[test]
724 fn message_rejects_field_named_message() {
725 let err = expand_message(quote! {}, quote! { struct M { message: usize } }).unwrap_err();
726 assert!(err.to_string().contains("must not be named `message`"));
727 }
728
729 #[test]
730 fn message_rejects_generic_params() {
731 let cases = [
732 quote! { struct M<T> { a: T } },
733 quote! { struct M<'a> { a: &'a str } },
734 quote! { struct M<const N: usize> { a: usize } },
735 ];
736
737 for item in cases {
738 let err = expand_message(quote! {}, item).unwrap_err();
739 assert!(err.to_string().contains("generic parameters"));
740 }
741 }
742
743 #[test]
744 fn message_escapes_braces_in_recorded_msg() {
745 let expanded = expand_message(
746 quote! { msg = "rate {limit} hit" },
747 quote! { struct M { a: usize } },
748 )
749 .expect("expansion succeeds");
750 let pretty = prettyplease::unparse(&syn::parse2::<syn::File>(expanded).unwrap());
751
752 assert!(pretty.contains(r#""rate {limit} hit""#), "{pretty}");
755 assert!(pretty.contains(r#""rate {{limit}} hit""#), "{pretty}");
756 }
757
758 #[test]
759 fn message_rejects_non_ident_meta_key() {
760 let err =
761 expand_message(quote! { foo::bar = 1 }, quote! { struct M { a: usize } }).unwrap_err();
762 assert!(err.to_string().contains("must be identifiers"));
763 }
764
765 #[test]
766 fn message_rejects_non_string_msg() {
767 let err = expand_message(quote! { msg = 5 }, quote! { struct M { a: usize } }).unwrap_err();
768 assert!(err.to_string().contains("must be a string literal"));
769 }
770
771 #[test]
772 fn message_rejects_unknown_level() {
773 let err =
774 expand_message(quote! { level = bogus }, quote! { struct M { a: usize } }).unwrap_err();
775 assert!(err.to_string().contains("unknown level"));
776 }
777
778 #[test]
779 fn message_sorts_and_dedups_tags() {
780 let expanded = expand_message(
781 quote! { msg = "m", tags = ["b", "a", "a"] },
782 quote! { struct M { x: usize } },
783 )
784 .expect("expansion succeeds");
785 let file = syn::parse2::<syn::File>(expanded).expect("output is valid Rust");
786 let pretty = prettyplease::unparse(&file);
787 assert!(pretty.contains("pub const TAGS"));
788 assert!(
789 pretty.contains(r#"["a", "b"]"#),
790 "tags sorted+deduped: {pretty}"
791 );
792 }
793
794 #[test]
795 fn message_emits_origin_const() {
796 let expanded =
797 expand_message(quote! { msg = "m" }, quote! { struct M { x: usize } }).unwrap();
798 let pretty = prettyplease::unparse(&syn::parse2::<syn::File>(expanded).unwrap());
799 assert!(pretty.contains("pub const ORIGIN"));
800 assert!(pretty.contains("CARGO_PKG_NAME"));
801 assert!(pretty.contains("pub const TAGS"));
802 }
803
804 #[test]
805 fn message_rejects_non_lowercase_tag() {
806 let err = expand_message(
807 quote! { tags = ["Security"] },
808 quote! { struct M { a: usize } },
809 )
810 .unwrap_err();
811 let msg = err.to_string();
812 assert!(msg.contains("lowercase"), "{msg}");
813 assert!(msg.contains("security"), "{msg}");
814 }
815
816 #[test]
817 fn message_rejects_empty_tag() {
818 let err =
819 expand_message(quote! { tags = [""] }, quote! { struct M { a: usize } }).unwrap_err();
820 assert!(err.to_string().contains("must not be empty"));
821 }
822
823 #[test]
824 fn message_rejects_non_string_tag() {
825 let err =
826 expand_message(quote! { tags = [1] }, quote! { struct M { a: usize } }).unwrap_err();
827 assert!(err.to_string().contains("string literal"));
828 }
829
830 #[test]
831 fn message_rejects_non_array_tags() {
832 let err = expand_message(
833 quote! { tags = "security" },
834 quote! { struct M { a: usize } },
835 )
836 .unwrap_err();
837 assert!(err.to_string().contains("array of string literals"));
838 }
839}