From aeffec1761ed07feda327991ac8fc5d49d62883a Mon Sep 17 00:00:00 2001 From: Lohit Kolluri Date: Thu, 9 Jul 2026 12:56:36 +0530 Subject: [PATCH] fix(derive-encode): emit spanned compile errors Use syn::Error::new_spanned(...).to_compile_error() for all invalid input shapes so diagnostics point at the offending type or attribute value (e.g. the unknown ident in #[prometheus(unknown)]), rather than the derive site or a proc-macro panic. This also removes the double-parse + unwrap for #[prometheus(...)] by parsing the attribute once during field processing. Signed-off-by: Lohit Kolluri --- derive-encode/src/lib.rs | 123 +++++++++++------- .../build/friendly-compilation-error-msg.rs | 37 ++++++ .../friendly-compilation-error-msg.stderr | 41 ++++++ derive-encode/tests/lib.rs | 3 +- 4 files changed, 159 insertions(+), 45 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..ebc131fb 100644 --- a/derive-encode/src/lib.rs +++ b/derive-encode/src/lib.rs @@ -11,6 +11,12 @@ use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::DeriveInput; +fn error_spanned(tokens: &T, msg: &str) -> TokenStream { + syn::Error::new_spanned(tokens, msg) + .to_compile_error() + .into() +} + /// Derive `prometheus_client::encoding::EncodeLabelSet`. #[proc_macro_derive(EncodeLabelSet, attributes(prometheus))] pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { @@ -19,55 +25,82 @@ pub fn derive_encode_label_set(input: TokenStream) -> TokenStream { let body: TokenStream2 = match ast.clone().data { syn::Data::Struct(s) => match s.fields { - syn::Fields::Named(syn::FieldsNamed { named, .. }) => named - .into_iter() - .map(|f| { - let attribute = f - .attrs - .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 ident = f.ident.unwrap(); - if flatten { - quote! { - EncodeLabelSet::encode(&self.#ident, encoder)?; - } - } else { - let ident_string = KEYWORD_IDENTIFIERS + syn::Fields::Named(syn::FieldsNamed { named, .. }) => { + let body = named + .into_iter() + .map(|f| { + let ident = f.ident.unwrap(); + let flatten = match f + .attrs .iter() - .find(|pair| ident == pair.1) - .map(|pair| pair.0.to_string()) - .unwrap_or_else(|| ident.to_string()); - - quote! { - let mut label_encoder = encoder.encode_label(); - let mut label_key_encoder = label_encoder.encode_label_key()?; - EncodeLabelKey::encode(&#ident_string, &mut label_key_encoder)?; - - let mut label_value_encoder = label_key_encoder.encode_label_value()?; - EncodeLabelValue::encode(&self.#ident, &mut label_value_encoder)?; - - label_value_encoder.finish()?; + .find(|a| a.path().is_ident("prometheus")) + { + None => false, + Some(a) => match a.parse_args::() { + Ok(ident) if ident == "flatten" => true, + Ok(other) => { + return Err(error_spanned( + &other, + &format!( + "Provided attribute '{other}', but only 'flatten' is supported" + ), + )); + } + Err(_) => { + return Err(error_spanned( + a, + "Attribute on `#[prometheus(...)]` must be an identifier, e.g. `#[prometheus(flatten)]`", + )); + } + }, + }; + + if flatten { + Ok(quote! { + EncodeLabelSet::encode(&self.#ident, encoder)?; + }) + } else { + let ident_string = KEYWORD_IDENTIFIERS + .iter() + .find(|pair| ident == pair.1) + .map(|pair| pair.0.to_string()) + .unwrap_or_else(|| ident.to_string()); + + Ok(quote! { + let mut label_encoder = encoder.encode_label(); + let mut label_key_encoder = label_encoder.encode_label_key()?; + EncodeLabelKey::encode(&#ident_string, &mut label_key_encoder)?; + + let mut label_value_encoder = label_key_encoder.encode_label_value()?; + EncodeLabelValue::encode(&self.#ident, &mut label_value_encoder)?; + + label_value_encoder.finish()?; + }) } - } - }) - .collect(), + }) + .collect::>(); + + match body { + Ok(body) => body, + Err(err) => return err, + } + } syn::Fields::Unnamed(_) => { - panic!("Can not derive Encode for struct with unnamed fields.") + return error_spanned( + &ast, + "Can not derive Encode for struct with unnamed fields.", + ); + } + syn::Fields::Unit => { + return error_spanned(&ast, "Can not derive Encode for struct with unit field."); } - 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 error_spanned(&ast, "Can not derive Encode for enum."); + } + syn::Data::Union(_) => { + return error_spanned(&ast, "Can not derive Encode for union."); } - syn::Data::Union(_) => panic!("Can not derive Encode for union."), }; let gen = quote! { @@ -95,7 +128,7 @@ 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 error_spanned(&ast, "Can not derive EncodeLabel for struct."); } syn::Data::Enum(syn::DataEnum { variants, .. }) => { let match_arms: TokenStream2 = variants @@ -114,7 +147,9 @@ pub fn derive_encode_label_value(input: TokenStream) -> TokenStream { } } } - syn::Data::Union(_) => panic!("Can not derive Encode for union."), + syn::Data::Union(_) => { + return error_spanned(&ast, "Can not derive Encode for union."); + } }; 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..2da32a33 --- /dev/null +++ b/derive-encode/tests/build/friendly-compilation-error-msg.stderr @@ -0,0 +1,41 @@ +error: Can not derive Encode for struct with unnamed fields. + --> tests/build/friendly-compilation-error-msg.rs:6:1 + | +6 | struct Unnamed(String); + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: Can not derive Encode for struct with unit field. + --> tests/build/friendly-compilation-error-msg.rs:10:1 + | +10 | struct UnitStruct; + | ^^^^^^^^^^^^^^^^^^ + +error: Can not derive Encode for enum. + --> tests/build/friendly-compilation-error-msg.rs:14:1 + | +14 | / enum NotAllowedSet { +15 | | A, +16 | | B, +17 | | } + | |_^ + +error: Provided attribute 'unknown', but only 'flatten' is supported + --> tests/build/friendly-compilation-error-msg.rs:22:18 + | +22 | #[prometheus(unknown)] + | ^^^^^^^ + +error: Can not derive EncodeLabel for struct. + --> tests/build/friendly-compilation-error-msg.rs:28:1 + | +28 | struct StructForValue; + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: Can not derive Encode for union. + --> tests/build/friendly-compilation-error-msg.rs:32:1 + | +32 | / union UnionSet { +33 | | a: u32, +34 | | b: f32, +35 | | } + | |_^ 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"); }