From 9fcbe1ba52f34d9588307bd890a3d08a90c2f8e4 Mon Sep 17 00:00:00 2001 From: Lohit Kolluri Date: Mon, 22 Jun 2026 15:21:13 +0530 Subject: [PATCH] fix(derive-encode): emit friendly compile errors instead of panicking The proc-macro derives panicked for unsupported shapes (unnamed/unit struct, enum, union, unknown `#[prometheus(...)] attribute) and the bad-attribute case was inside a closure, which produced a confusing `error: proc-macro derive panicked` diagnostic that pointed users at the proc-macro internals rather than the `#[derive(...)] line they needed to fix. Replace all `panic!` calls with `compile_error!` so users see a direct diagnostic that points to the offending `#[derive(...)] line. For the unknown-attribute case, validate attributes in a pre-pass so the derive can return a `compile_error!` token stream before generating the impl. Add a trybuild `compile_fail` test that exercises each error path and pins the expected diagnostics. Signed-off-by: Lohit Kolluri --- derive-encode/src/lib.rs | 71 +++++++++++++++---- .../build/friendly-compilation-error-msg.rs | 37 ++++++++++ .../friendly-compilation-error-msg.stderr | 47 ++++++++++++ derive-encode/tests/lib.rs | 3 +- 4 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 derive-encode/tests/build/friendly-compilation-error-msg.rs create mode 100644 derive-encode/tests/build/friendly-compilation-error-msg.stderr diff --git a/derive-encode/src/lib.rs b/derive-encode/src/lib.rs index a3c0aac3..0f750ee3 100644 --- a/derive-encode/src/lib.rs +++ b/derive-encode/src/lib.rs @@ -17,6 +17,33 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { let ast: DeriveInput = syn::parse(input).unwrap(); let name = &ast.ident; + if let syn::Data::Struct(syn::DataStruct { ref fields, .. }) = ast.data { + for f in fields.iter() { + for a in &f.attrs { + if !a.path().is_ident("prometheus") { + continue; + } + match a.parse_args::() { + Ok(ident) if ident == "flatten" => {} + Ok(other) => { + let msg = format!( + "Provided attribute '{other}', but only 'flatten' is supported" + ); + return quote! { compile_error!(#msg); }.into(); + } + Err(_) => { + return quote! { + compile_error!( + "Attribute on `#[prometheus(...)]` must be an identifier, e.g. `#[prometheus(flatten)]`" + ); + } + .into(); + } + } + } + } + } + let body: TokenStream2 = match ast.clone().data { syn::Data::Struct(s) => match s.fields { syn::Fields::Named(syn::FieldsNamed { named, .. }) => named @@ -27,13 +54,7 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { .iter() .find(|a| a.path().is_ident("prometheus")) .map(|a| a.parse_args::().unwrap().to_string()); - let flatten = match attribute.as_deref() { - Some("flatten") => true, - Some(other) => { - panic!("Provided attribute '{other}', but only 'flatten' is supported") - } - None => false, - }; + let flatten = attribute.as_deref() == Some("flatten"); let ident = f.ident.unwrap(); if flatten { quote! { @@ -60,14 +81,30 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { }) .collect(), syn::Fields::Unnamed(_) => { - panic!("Can not derive Encode for struct with unnamed fields.") + return quote! { + compile_error!("Can not derive Encode for struct with unnamed fields."); + } + .into(); + } + syn::Fields::Unit => { + return quote! { + compile_error!("Can not derive Encode for struct with unit field."); + } + .into(); } - syn::Fields::Unit => panic!("Can not derive Encode for struct with unit field."), }, syn::Data::Enum(syn::DataEnum { .. }) => { - panic!("Can not derive Encode for enum.") + return quote! { + compile_error!("Can not derive Encode for enum."); + } + .into(); + } + syn::Data::Union(_) => { + return quote! { + compile_error!("Can not derive Encode for union."); + } + .into(); } - syn::Data::Union(_) => panic!("Can not derive Encode for union."), }; let gen = quote! { @@ -95,7 +132,10 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream { let body = match ast.clone().data { syn::Data::Struct(_) => { - panic!("Can not derive EncodeLabel for struct.") + return quote! { + compile_error!("Can not derive EncodeLabel for struct."); + } + .into(); } syn::Data::Enum(syn::DataEnum { variants, .. }) => { let match_arms: TokenStream2 = variants @@ -114,7 +154,12 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream { } } } - syn::Data::Union(_) => panic!("Can not derive Encode for union."), + syn::Data::Union(_) => { + return quote! { + compile_error!("Can not derive Encode for union."); + } + .into(); + } }; let gen = quote! { diff --git a/derive-encode/tests/build/friendly-compilation-error-msg.rs b/derive-encode/tests/build/friendly-compilation-error-msg.rs new file mode 100644 index 00000000..35149040 --- /dev/null +++ b/derive-encode/tests/build/friendly-compilation-error-msg.rs @@ -0,0 +1,37 @@ +use prometheus_client::encoding::EncodeLabelSet; +use prometheus_client::encoding::EncodeLabelValue; + +// Unnamed struct fields: not supported by `EncodeLabelSet`. +#[derive(EncodeLabelSet)] +struct Unnamed(String); + +// Unit struct: not supported by `EncodeLabelSet`. +#[derive(EncodeLabelSet)] +struct UnitStruct; + +// Enum: not supported by `EncodeLabelSet`. +#[derive(EncodeLabelSet)] +enum NotAllowedSet { + A, + B, +} + +// Unknown attribute: only `#[prometheus(flatten)]` is accepted. +#[derive(EncodeLabelSet)] +struct BadAttr { + #[prometheus(unknown)] + a: u64, +} + +// Struct: not supported by `EncodeLabelValue` (only enums are). +#[derive(EncodeLabelValue)] +struct StructForValue; + +// Union: not supported by either derive. +#[derive(EncodeLabelSet)] +union UnionSet { + a: u32, + b: f32, +} + +fn main() {} diff --git a/derive-encode/tests/build/friendly-compilation-error-msg.stderr b/derive-encode/tests/build/friendly-compilation-error-msg.stderr new file mode 100644 index 00000000..c7fd464c --- /dev/null +++ b/derive-encode/tests/build/friendly-compilation-error-msg.stderr @@ -0,0 +1,47 @@ +error: Can not derive Encode for struct with unnamed fields. + --> tests/build/friendly-compilation-error-msg.rs:5:10 + | +5 | #[derive(EncodeLabelSet)] + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `EncodeLabelSet` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Can not derive Encode for struct with unit field. + --> tests/build/friendly-compilation-error-msg.rs:9:10 + | +9 | #[derive(EncodeLabelSet)] + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `EncodeLabelSet` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Can not derive Encode for enum. + --> tests/build/friendly-compilation-error-msg.rs:13:10 + | +13 | #[derive(EncodeLabelSet)] + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `EncodeLabelSet` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Provided attribute 'unknown', but only 'flatten' is supported + --> tests/build/friendly-compilation-error-msg.rs:20:10 + | +20 | #[derive(EncodeLabelSet)] + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `EncodeLabelSet` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Can not derive EncodeLabel for struct. + --> tests/build/friendly-compilation-error-msg.rs:27:10 + | +27 | #[derive(EncodeLabelValue)] + | ^^^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `EncodeLabelValue` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Can not derive Encode for union. + --> tests/build/friendly-compilation-error-msg.rs:31:10 + | +31 | #[derive(EncodeLabelSet)] + | ^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `EncodeLabelSet` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/derive-encode/tests/lib.rs b/derive-encode/tests/lib.rs index ec30a562..fe4f9e20 100644 --- a/derive-encode/tests/lib.rs +++ b/derive-encode/tests/lib.rs @@ -205,5 +205,6 @@ fn flatten() { #[test] fn build() { let t = trybuild::TestCases::new(); - t.pass("tests/build/redefine-prelude-symbols.rs") + t.pass("tests/build/redefine-prelude-symbols.rs"); + t.compile_fail("tests/build/friendly-compilation-error-msg.rs"); }