Refactor attribute parsing in pmp-emitter-core: enforce strict types, add StrictBool, StrictString, and StrictPath wrappers, and improve error reporting

This commit is contained in:
2026-03-04 13:25:12 +01:00
parent 134e35cc9e
commit 6863d7377b

View File

@@ -29,6 +29,12 @@ struct StrictBool(bool);
impl FromMeta for StrictBool impl FromMeta for StrictBool
{ {
fn from_word() -> Result<Self> {
Err(Error::custom(
"expected `= true/false`, provide a boolean value"
))
}
fn from_value(value: &Lit) -> Result<Self> fn from_value(value: &Lit) -> Result<Self>
{ {
match value { match value {
@@ -36,22 +42,59 @@ impl FromMeta for StrictBool
_ => Err(Error::unexpected_lit_type(value)), _ => Err(Error::unexpected_lit_type(value)),
} }
} }
}
// intentionally NOT implementing from_word() - bare `exit` is rejected /// A string that must be written as `= "..."`
#[derive(Debug, Clone)]
struct StrictString(String);
impl FromMeta for StrictString
{
fn from_word() -> Result<Self> {
Err(Error::custom(
"expected `= \"...\"`, provide a string value"
))
}
fn from_value(value: &Lit) -> Result<Self> {
String::from_value(value).map(StrictString)
}
}
/// A path that must be written as `= my_fn`
#[derive(Debug, Clone)]
struct StrictPath(Path);
impl FromMeta for StrictPath
{
fn from_word() -> Result<Self> {
Err(Error::custom(
"expected `= my_fn`, provide a function path"
))
}
fn from_value(value: &Lit) -> Result<Self> {
Path::from_value(value).map(StrictPath)
}
fn from_expr(expr: &syn::Expr) -> Result<Self> {
Path::from_expr(expr).map(StrictPath)
}
} }
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
/// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name) /// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name)
/// - `exit = true|false` - whether to call process::exit (default: false) /// - `exit = true|false` - whether to call process::exit (default: false)
/// - `colorizer = fn` - fn(String) -> String applied to all variants unless overridden
#[derive(Debug, Default, FromMeta)] #[derive(Debug, Default, FromMeta)]
struct EnumAttrs { struct EnumAttrs {
#[darling(default)] #[darling(default)]
exit: Option<StrictBool>, exit: Option<StrictBool>,
#[darling(default)] #[darling(default)]
colorizer: Option<Path>, colorizer: Option<StrictPath>,
#[darling(default)] #[darling(default)]
descriptor: Option<String>, descriptor: Option<StrictString>,
} }
/// - `message = "..."` - error message (default: variant name as title case) /// - `message = "..."` - error message (default: variant name as title case)
@@ -62,9 +105,9 @@ struct VariantAttrs {
#[darling(default)] #[darling(default)]
exit: Option<StrictBool>, exit: Option<StrictBool>,
#[darling(default)] #[darling(default)]
message: Option<String>, message: Option<StrictString>,
#[darling(default)] #[darling(default)]
colorizer: Option<Path>, colorizer: Option<StrictPath>,
} }
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
@@ -106,8 +149,10 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
let enum_ident = &shape.ident; let enum_ident = &shape.ident;
let descriptor = enum_attrs.descriptor let descriptor = enum_attrs.descriptor
.map(|s| s.0)
.unwrap_or_else(|| pascal_to_title(&enum_ident.to_string())); .unwrap_or_else(|| pascal_to_title(&enum_ident.to_string()));
let global_exit = enum_attrs.exit.map(|b| b.0).unwrap_or(false);
let global_exit = enum_attrs.exit.map(|b| b.0).unwrap_or(false);
let global_colorizer = enum_attrs.colorizer; let global_colorizer = enum_attrs.colorizer;
// Reject non-enums with a proper span pointing at the type name. // Reject non-enums with a proper span pointing at the type name.
@@ -117,7 +162,7 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
// Accumulate all per-variant attribute errors before failing, so the user // Accumulate all per-variant attribute errors before failing, so the user
// sees every problem in one compilation instead of one-at-a-time. // sees every problem in one compilation instead of one-at-a-time.
let mut errors: Option<syn::Error> = None; let mut errors: Option<syn::Error> = None;
let mut match_arms: Vec<TokenStream2> = Vec::with_capacity(variants.len()); let mut match_arms: Vec<TokenStream2> = Vec::with_capacity(variants.len());
for v in &variants for v in &variants
@@ -128,7 +173,8 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
let v_attrs: VariantAttrs = match parse_pmp_attr(&v.attrs) let v_attrs: VariantAttrs = match parse_pmp_attr(&v.attrs)
{ {
Ok(a) => a, Ok(a) => a,
Err(e) => { Err(e) =>
{
match &mut errors { match &mut errors {
None => errors = Some(e), None => errors = Some(e),
Some(existing) => existing.combine(e), Some(existing) => existing.combine(e),
@@ -137,7 +183,7 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
} }
}; };
let message = v_attrs.message.unwrap_or_else(|| title.clone()); let message = v_attrs.message.map(|s| s.0).unwrap_or_else(|| title.clone());
let should_exit = v_attrs.exit.map(|b| b.0).unwrap_or(global_exit); let should_exit = v_attrs.exit.map(|b| b.0).unwrap_or(global_exit);
// ┌─ Output format ───────────────────────────────┐ // ┌─ Output format ───────────────────────────────┐
@@ -148,8 +194,8 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
let header = format!("{}: {} (\"{}\")", descriptor, title, message); let header = format!("{}: {} (\"{}\")", descriptor, title, message);
let formatted_header = match v_attrs.colorizer.as_ref().or(global_colorizer.as_ref()) { let formatted_header = match v_attrs.colorizer.as_ref().or(global_colorizer.as_ref()) {
Some(path) => quote! { #path(#header) }, Some(StrictPath(path)) => quote! { #path(#header) },
None => quote! { #header.to_string() }, None => quote! { #header.to_string() },
}; };
let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} }; let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} };
@@ -158,14 +204,21 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
// source location instead of pointing at generated code. // source location instead of pointing at generated code.
let arm = match v.fields.style let arm = match v.fields.style
{ {
ast::Style::Tuple => quote_spanned! { var_ident.span() => ast::Style::Tuple =>
#enum_ident::#var_ident(trace) => { {
println!("{}\n\n{}", #formatted_header, trace); let pattern = if v.fields.len() == 1 { quote! { (trace) } } else { quote! { (trace, ..) } };
#exit_tokens
quote_spanned! { var_ident.span() =>
#enum_ident::#var_ident #pattern =>
{
println!("{}\n\n{}", #formatted_header, trace);
#exit_tokens
}
} }
}, },
_ => quote_spanned! { var_ident.span() => _ => quote_spanned! { var_ident.span() =>
#enum_ident::#var_ident => { #enum_ident::#var_ident =>
{
println!("{}", #formatted_header); println!("{}", #formatted_header);
#exit_tokens #exit_tokens
} }