Compare commits

..

2 Commits

View File

@@ -1,6 +1,7 @@
use quote::quote;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use syn::{ Lit, Path, Type, Ident, DeriveInput, parse_macro_input }; use quote::{ quote, quote_spanned };
use syn::{ Lit, Path, Type, Ident, DeriveInput };
use proc_macro2::{ TokenStream as TokenStream2 };
use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput }; use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput };
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
@@ -22,36 +23,18 @@ struct VariantReceiver {
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
/// A boolean that must be written as `= true` or `= false`
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct StrictBool(bool); struct StrictBool(bool);
/// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name)
/// - `exit = true|false` - whether to call process::exit (default: false)
#[derive(Debug, FromMeta)]
struct EnumAttrs {
#[darling(default)]
descriptor: Option<String>,
#[darling(default)]
exit: Option<StrictBool>,
}
/// - `message = "..."` - error message (default: variant name as title case)
/// - `colorizer = fn` - optional fn(String) -> String for formatting
/// - `exit = true|false` - overrides enum-level exit
#[derive(Debug, FromMeta)]
struct VariantAttrs {
#[darling(default)]
exit: Option<StrictBool>,
#[darling(default)]
message: Option<String>,
#[darling(default)]
colorizer: Option<Path>,
}
// ---------------------------------------------------------------------------------------------- //
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 {
@@ -59,31 +42,72 @@ impl FromMeta for StrictBool
_ => Err(Error::unexpected_lit_type(value)), _ => Err(Error::unexpected_lit_type(value)),
} }
} }
// intentionally NOT implementing from_word()
} }
fn parse_pmp_attr<T: FromMeta>(attrs: &[syn::Attribute]) -> T /// A string that must be written as `= "..."`
where #[derive(Debug, Clone)]
T: Default, struct StrictString(String);
impl FromMeta for StrictString
{ {
attrs fn from_word() -> Result<Self> {
.iter() Err(Error::custom(
.find(|a| a.path().is_ident("pmp_emitter")) "expected `= \"...\"`, provide a string value"
.map(|a| T::from_meta(&a.meta).expect("invalid pmp_emitter attribute")) ))
.unwrap_or_default()
} }
impl Default for EnumAttrs { fn from_value(value: &Lit) -> Result<Self> {
fn default() -> Self { String::from_value(value).map(StrictString)
Self { descriptor: None, exit: None }
} }
} }
impl Default for VariantAttrs { /// A path that must be written as `= my_fn`
fn default() -> Self { #[derive(Debug, Clone)]
Self { message: None, exit: None, colorizer: None } 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)
/// - `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)]
struct EnumAttrs {
#[darling(default)]
exit: Option<StrictBool>,
#[darling(default)]
colorizer: Option<StrictPath>,
#[darling(default)]
descriptor: Option<StrictString>,
}
/// - `message = "..."` - error message (default: variant name as title case)
/// - `colorizer = fn` - optional fn(String) -> String for formatting
/// - `exit = true|false` - overrides enum-level exit
#[derive(Debug, Default, FromMeta)]
struct VariantAttrs {
#[darling(default)]
exit: Option<StrictBool>,
#[darling(default)]
message: Option<StrictString>,
#[darling(default)]
colorizer: Option<StrictPath>,
} }
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
@@ -92,8 +116,7 @@ fn pascal_to_title(s: &str) -> String
{ {
let mut out = String::new(); let mut out = String::new();
for (i, ch) in s.chars().enumerate() for (i, ch) in s.chars().enumerate() {
{
if ch.is_uppercase() && i > 0 { out.push(' '); } if ch.is_uppercase() && i > 0 { out.push(' '); }
out.push(ch); out.push(ch);
} }
@@ -101,42 +124,67 @@ fn pascal_to_title(s: &str) -> String
out out
} }
/// Parse a `#[pmp_emitter(...)]` attribute from the given list into `T`.
///
/// Returns `Ok(T::default())` if no such attribute is present or
/// a span-accurate `Err` if the attribute is present but malformed.
fn parse_pmp_attr<T>(attrs: &[syn::Attribute]) -> syn::Result<T>
where
T: FromMeta + Default,
{
match attrs.iter().find(|a| a.path().is_ident("pmp_emitter")) {
None => Ok(T::default()),
Some(attr) => T::from_meta(&attr.meta)
.map_err(|e| syn::Error::new_spanned(attr, e.to_string())),
}
}
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
#[proc_macro_derive(PmpEmitter, attributes(pmp_emitter))] /// All real expansion logic lives here, separate from the proc-macro entry point,
pub fn pmp_emitter(item: TokenStream) -> TokenStream /// so it can be called and tested without the proc-macro machinery.
fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStream2>
{ {
let input = parse_macro_input!(item as DeriveInput); let enum_attrs: EnumAttrs = parse_pmp_attr(&raw_input.attrs)?;
// parse enum shape
let shape = match EmitterInput::from_derive_input(&input) {
Ok(a) => a,
Err(e) => return e.write_errors().into(),
};
// parse enum-level #[pmp_emitter(...)] directly from the raw attrs
let enum_attrs: EnumAttrs = parse_pmp_attr(&input.attrs);
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 variants = shape // Reject non-enums with a proper span pointing at the type name.
.data let variants = shape.data.take_enum().ok_or_else(|| {
.take_enum() syn::Error::new_spanned(enum_ident, "PmpEmitter can only be derived on enums")
.expect("PmpEmitter can only be derived on enums"); })?;
let match_arms = variants.iter().map(|v| // Accumulate all per-variant attribute errors before failing, so the user
// sees every problem in one compilation instead of one-at-a-time.
let mut errors: Option<syn::Error> = None;
let mut match_arms: Vec<TokenStream2> = Vec::with_capacity(variants.len());
for v in &variants
{ {
let var_ident = &v.ident; let var_ident = &v.ident;
let title = pascal_to_title(&var_ident.to_string()); let title = pascal_to_title(&var_ident.to_string());
// parse variant-level #[pmp_emitter(...)] from the variant's raw attrs let v_attrs: VariantAttrs = match parse_pmp_attr(&v.attrs)
let vattrs: VariantAttrs = parse_pmp_attr(&v.attrs); {
Ok(a) => a,
Err(e) =>
{
match &mut errors {
None => errors = Some(e),
Some(existing) => existing.combine(e),
}
continue; // keep parsing remaining variants
}
};
let message = vattrs.message.unwrap_or_else(|| title.clone()); let message = v_attrs.message.map(|s| s.0).unwrap_or_else(|| title.clone());
let should_exit = vattrs.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 ───────────────────────────────┐
// │ {descriptor}: {Variant Title} ("{message}") │ // │ {descriptor}: {Variant Title} ("{message}") │
@@ -145,34 +193,46 @@ pub fn pmp_emitter(item: TokenStream) -> TokenStream
// └───────────────────────────────────────────────┘ // └───────────────────────────────────────────────┘
let header = format!("{}: {} (\"{}\")", descriptor, title, message); let header = format!("{}: {} (\"{}\")", descriptor, title, message);
let formatted_header = match &vattrs.colorizer let formatted_header = match v_attrs.colorizer.as_ref().or(global_colorizer.as_ref()) {
{ Some(StrictPath(path)) => quote! { #path(#header) },
Some(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! {} };
match v.fields.style // quote_spanned! ties any downstream type errors back to the variant's
// source location instead of pointing at generated code.
let arm = match v.fields.style
{ {
ast::Style::Tuple => quote! { ast::Style::Tuple =>
#enum_ident::#var_ident(trace) => {
let pattern = if v.fields.len() == 1 { quote! { (trace) } } else { quote! { (trace, ..) } };
quote_spanned! { var_ident.span() =>
#enum_ident::#var_ident #pattern =>
{ {
println!("{}\n\n{}", #formatted_header, trace); println!("{}\n\n{}", #formatted_header, trace);
#exit_tokens #exit_tokens
} }
}
}, },
_ => quote! { _ => quote_spanned! { var_ident.span() =>
#enum_ident::#var_ident => #enum_ident::#var_ident =>
{ {
println!("{}", #formatted_header); println!("{}", #formatted_header);
#exit_tokens #exit_tokens
} }
}, },
} };
});
quote! { match_arms.push(arm);
}
// Surface all accumulated variant errors at once.
if let Some(e) = errors { return Err(e); }
Ok(quote!
{
impl #enum_ident impl #enum_ident
{ {
pub fn emit(&self) { pub fn emit(&self) {
@@ -186,5 +246,23 @@ pub fn pmp_emitter(item: TokenStream) -> TokenStream
self.emit() self.emit()
} }
} }
}.into() })
}
// ---------------------------------------------------------------------------------------------- //
/// Thin entry point: parse -> delegate -> convert any error to `compile_error!`.
#[proc_macro_derive(PmpEmitter, attributes(pmp_emitter))]
pub fn pmp_emitter(item: TokenStream) -> TokenStream
{
let input = syn::parse_macro_input!(item as DeriveInput);
let shape = match EmitterInput::from_derive_input(&input) {
Ok(s) => s,
Err(e) => return e.write_errors().into(),
};
expand(shape, &input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
} }