use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
DeriveInput, Token,
};
struct ParsedErrors {
ident: syn::Ident,
generics: syn::Generics,
variants: Vec<Variant>,
}
enum Message {
None,
Single(String),
Multiline(String, String),
}
struct Variant {
ident: syn::Ident,
fields: syn::Fields,
msg: Message,
from: bool,
}
struct AttrArg {
ident: syn::Ident,
value: Option<syn::Expr>,
}
impl Parse for AttrArg {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let ident = input.parse()?;
let value = if input.parse::<Token![=]>().is_ok() {
input.parse::<syn::Expr>().ok()
} else {
None
};
Ok(Self { ident, value })
}
}
struct AttrArgs(Vec<AttrArg>);
impl Parse for AttrArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut args = vec![];
loop {
args.push(input.parse()?);
if input.parse::<Token![,]>().is_err() {
return Ok(Self(args));
}
}
}
}
fn parse_attr_doc(a: &syn::Attribute) -> Option<&syn::Expr> {
if !matches!(a.style, syn::AttrStyle::Outer) {
return None;
}
let syn::Meta::NameValue(ref nameval) = a.meta else {
return None;
};
if !nameval.path.is_ident("doc") {
return None;
}
Some(&nameval.value)
}
fn parse_attr(a: &syn::Attribute) -> Option<AttrArgs> {
if !matches!(a.style, syn::AttrStyle::Outer) {
return None;
}
let syn::Meta::List(ref list) = a.meta else {
return None;
};
if !matches!(list.delimiter, syn::MacroDelimiter::Paren(_)) {
return None;
}
if !list.path.is_ident("err") {
return None;
}
Some(list.parse_args().expect("could not parse attr args"))
}
fn expr_str(a: &syn::Expr) -> Option<String> {
match a {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => Some(s.value()),
_ => None,
}
.map(|s| s.strip_prefix(' ').unwrap_or(&s).to_string())
}
fn parse_variant(v: syn::Variant) -> Variant {
let mut doc = v
.attrs
.iter()
.filter_map(parse_attr_doc)
.filter_map(expr_str);
let mut args = v.attrs.iter().filter_map(parse_attr);
let amsg = args
.clone()
.filter_map(|a| {
a.0.into_iter()
.find(|a| a.ident == "msg")
.and_then(|a| a.value)
})
.next_back()
.and_then(|e| expr_str(&e));
let msg = if let Some(amsg) = amsg {
Message::Single(amsg)
} else if let Some(msg) = doc.next() {
let extra: Vec<_> = doc.collect();
if extra.is_empty() {
Message::Single(msg)
} else {
Message::Multiline(msg, extra.join("\n"))
}
} else {
Message::None
};
let from = args
.find_map(|a| a.0.into_iter().find(|a| a.ident == "from"))
.is_some();
Variant {
ident: v.ident,
fields: v.fields,
msg,
from,
}
}
fn parse_derive(ast: DeriveInput) -> ParsedErrors {
let ident = ast.ident;
let generics = ast.generics;
let syn::Data::Enum(body) = ast.data else {
panic!("only enums are supported")
};
let variants = body.variants.into_iter().map(parse_variant).collect();
ParsedErrors {
ident,
generics,
variants,
}
}
fn generate(parsed: ParsedErrors) -> TokenStream {
let ParsedErrors {
ident,
generics,
variants,
} = parsed;
let arms = variants.iter().map(|v| {
let Variant {
ident: name,
fields,
msg,
..
} = v;
let bmsg = match msg {
Message::None => {
let name = name.to_string();
quote!(#name)
}
Message::Single(s) | Message::Multiline(s, _) => quote!(#s),
};
let mut set = quote!();
let mut get = vec![];
let mut fmt = vec![quote!("{}")];
if !matches!(fields, syn::Fields::Unit) {
fmt.push(quote!(":"));
}
if matches!(msg, Message::Multiline(_, _)) {
fmt.push(quote!("\n"));
} else if !matches!(fields, syn::Fields::Unit) {
fmt.push(quote!(" "));
}
match fields {
syn::Fields::Named(fields) => {
let mut ids = vec![];
for (fnum, field) in fields.named.iter().enumerate() {
let fid = syn::Ident::new(format!("arg_{fnum}").as_ref(), Span::call_site());
get.push(quote!(#fid));
let fnm = field.ident.as_ref().expect("missing ident");
ids.push(quote!(#fnm));
if fnum > 0 {
fmt.push(quote!(", "));
}
let fo = format!("{fnm}: {{}}");
fmt.push(quote!(#fo));
}
set = quote!({#(#ids: #get),*});
}
syn::Fields::Unnamed(fields) => {
for fnum in 0..fields.unnamed.len() {
let fid = syn::Ident::new(format!("arg_{fnum}").as_ref(), Span::call_site());
get.push(quote!(#fid));
if fnum > 0 {
fmt.push(quote!(", "));
}
fmt.push(quote!("{}"));
}
set = quote!((#(#get),*));
}
syn::Fields::Unit => (),
}
if let Message::Multiline(_, s) = msg {
fmt.push(quote!("\n{}"));
get.push(quote!(#s));
}
quote! {
#ident::#name #set => write!(f, concat!(#(#fmt),*), #bmsg, #(#get),*)
}
});
let froms = variants.iter().filter_map(|v| {
if !v.from {
return None;
}
let syn::Fields::Unnamed(ref fields) = v.fields else {
panic!("automatically deriving From is only supported for unnamed fields")
};
let [ref field] = fields.unnamed.iter().collect::<Vec<_>>()[..] else {
panic!("automatically deriving From is only supported with a single field")
};
let name = &v.ident;
Some(quote! {
#[automatically_derived]
impl #generics ::core::convert::From<#field> for #ident #generics {
fn from(inner: #field) -> Self {
Self::#name(inner)
}
}
})
});
quote! {
#[automatically_derived]
impl #generics ::core::fmt::Display for #ident #generics {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
#(#arms,)*
}
}
}
#[automatically_derived]
impl #generics ::core::error::Error for #ident #generics {}
#(#froms)*
}
}
#[allow(clippy::missing_panics_doc)]
#[proc_macro_derive(FoxError, attributes(err))]
pub fn foxerror(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = syn::parse(input).unwrap();
let parsed = parse_derive(input);
let output = generate(parsed);
output.into()
}