use std::collections::BTreeSet;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Fields, LitStr, Type, Variant};
pub(crate) fn expand(input: &DeriveInput) -> syn::Result<TokenStream> {
let Data::Enum(data) = &input.data else {
return Err(syn::Error::new(
input.ident.span(),
"#[derive(Error)] supports enums only; hand-write `Display`/`Error` for structs",
));
};
let variants = data
.variants
.iter()
.map(VariantModel::parse)
.collect::<syn::Result<Vec<_>>>()?;
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let display_arms = variants
.iter()
.map(VariantModel::display_arm)
.collect::<Vec<_>>();
let has_any_source = variants.iter().any(|v| v.source.is_some());
let error_impl = if has_any_source {
let source_arms = variants
.iter()
.map(VariantModel::source_arm)
.collect::<Vec<_>>();
quote! {
impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause {
fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> {
match self {
#(#source_arms)*
}
}
}
}
} else {
quote! {
impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause {}
}
};
let from_impls = variants
.iter()
.filter_map(|v| v.emit_from_impl(ident, &input.generics))
.collect::<Vec<_>>();
Ok(quote! {
impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
#(#display_arms)*
}
}
}
#error_impl
#(#from_impls)*
})
}
struct SourceField {
member: SourceMember,
is_box: bool,
ty: Type,
is_from: bool,
}
enum SourceMember {
Named(syn::Ident),
Indexed(usize),
}
struct VariantModel {
ident: syn::Ident,
fields: Fields,
format: FormatSpec,
source: Option<SourceField>,
}
struct FormatSpec {
original: LitStr,
rewritten: String,
used_names: BTreeSet<String>,
used_positions: BTreeSet<usize>,
}
impl VariantModel {
fn parse(variant: &Variant) -> syn::Result<Self> {
let lit = variant_error_literal(variant)?;
let format = rewrite_format(&lit)?;
let source = parse_source_field(variant)?;
Ok(Self {
ident: variant.ident.clone(),
fields: variant.fields.clone(),
format,
source,
})
}
fn display_arm(&self) -> TokenStream {
let vident = &self.ident;
let lit = self.format.literal();
match &self.fields {
Fields::Named(named) => {
let pats = named.named.iter().map(|f| {
let ident = f
.ident
.as_ref()
.expect("named field has an identifier by construction");
if self.format.used_names.contains(&ident.to_string()) {
quote!(#ident)
} else {
quote!(#ident: _)
}
});
quote! {
Self::#vident { #(#pats),* } => ::core::write!(f, #lit),
}
}
Fields::Unnamed(unnamed) => {
let pats = (0..unnamed.unnamed.len()).map(|i| {
if self.format.used_positions.contains(&i) {
let binding = format_ident!("__arg_{}", i);
quote!(#binding)
} else {
quote!(_)
}
});
quote! {
Self::#vident( #(#pats),* ) => ::core::write!(f, #lit),
}
}
Fields::Unit => quote! {
Self::#vident => ::core::write!(f, #lit),
},
}
}
fn source_arm(&self) -> TokenStream {
let vident = &self.ident;
match &self.source {
Some(source) => {
let expr = if source.is_box {
quote!(__source.as_ref())
} else {
quote!(__source)
};
match &source.member {
SourceMember::Named(name) => quote! {
Self::#vident { #name: __source, .. } =>
::core::option::Option::Some(#expr),
},
SourceMember::Indexed(index) => {
let count = match &self.fields {
Fields::Unnamed(u) => u.unnamed.len(),
Fields::Named(_) | Fields::Unit => 0,
};
let pats = (0..count).map(|i| {
if i == *index {
quote!(__source)
} else {
quote!(_)
}
});
quote! {
Self::#vident( #(#pats),* ) =>
::core::option::Option::Some(#expr),
}
}
}
}
None => match &self.fields {
Fields::Named(_) => quote! {
Self::#vident { .. } => ::core::option::Option::None,
},
Fields::Unnamed(_) => quote! {
Self::#vident( .. ) => ::core::option::Option::None,
},
Fields::Unit => quote! {
Self::#vident => ::core::option::Option::None,
},
},
}
}
fn emit_from_impl(
&self,
enum_ident: &syn::Ident,
generics: &syn::Generics,
) -> Option<TokenStream> {
let source = self.source.as_ref()?;
if !source.is_from {
return None;
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let vident = &self.ident;
let ty = &source.ty;
let construct = match &source.member {
SourceMember::Named(name) => quote!(Self::#vident { #name: __value }),
SourceMember::Indexed(_) => quote!(Self::#vident(__value)),
};
Some(quote! {
impl #impl_generics ::core::convert::From<#ty>
for #enum_ident #ty_generics #where_clause
{
fn from(__value: #ty) -> Self {
#construct
}
}
})
}
}
impl FormatSpec {
fn literal(&self) -> LitStr {
if self.rewritten == self.original.value() {
self.original.clone()
} else {
LitStr::new(&self.rewritten, self.original.span())
}
}
}
fn variant_error_literal(variant: &Variant) -> syn::Result<LitStr> {
let mut found: Option<LitStr> = None;
for attr in &variant.attrs {
if !attr.path().is_ident("error") {
continue;
}
if found.is_some() {
return Err(syn::Error::new(
attr.span(),
"duplicate `#[error(...)]` on this variant; exactly one is required",
));
}
found = Some(attr.parse_args::<LitStr>()?);
}
found.ok_or_else(|| {
syn::Error::new(
variant.ident.span(),
"every variant of a `#[derive(Error)]` enum needs an `#[error(\"...\")]` \
attribute; if a variant's Display cannot be a single format literal, \
hand-write the whole type instead",
)
})
}
fn parse_source_field(variant: &Variant) -> syn::Result<Option<SourceField>> {
let mut result: Option<SourceField> = None;
for (index, field) in variant.fields.iter().enumerate() {
let is_source = field.attrs.iter().any(|a| a.path().is_ident("source"));
let is_from = field.attrs.iter().any(|a| a.path().is_ident("from"));
if !is_source && !is_from {
continue;
}
if result.is_some() {
return Err(syn::Error::new(
field.span(),
"a variant may declare at most one `#[source]`/`#[from]` field",
));
}
if is_from && variant.fields.len() != 1 {
return Err(syn::Error::new(
field.span(),
"`#[from]` requires the variant to have exactly one field",
));
}
let member = match &field.ident {
Some(ident) => SourceMember::Named(ident.clone()),
None => SourceMember::Indexed(index),
};
result = Some(SourceField {
member,
is_box: type_is_box(&field.ty),
ty: field.ty.clone(),
is_from,
});
}
Ok(result)
}
fn type_is_box(ty: &Type) -> bool {
let Type::Path(path) = ty else {
return false;
};
path.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Box")
}
fn rewrite_format(lit: &LitStr) -> syn::Result<FormatSpec> {
let input = lit.value();
let mut out = String::with_capacity(input.len());
let mut used_names = BTreeSet::new();
let mut used_positions = BTreeSet::new();
let mut implicit = 0usize;
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'{' if chars.peek() == Some(&'{') => {
chars.next();
out.push_str("{{");
}
'}' if chars.peek() == Some(&'}') => {
chars.next();
out.push_str("}}");
}
'}' => {
return Err(syn::Error::new(
lit.span(),
"unmatched `}` in `#[error]` format string",
));
}
'{' => {
let (arg, spec) = read_placeholder(lit, &mut chars)?;
if spec.contains('$') {
return Err(syn::Error::new(
lit.span(),
"width/precision `$` arguments are not supported by #[derive(Error)]; \
inline the value or hand-write the type",
));
}
let reference = if arg.is_empty() {
let index = implicit;
implicit += 1;
used_positions.insert(index);
format!("__arg_{index}")
} else if arg.bytes().all(|b| b.is_ascii_digit()) {
let index: usize = arg.parse().map_err(|_| {
syn::Error::new(lit.span(), "invalid positional index in `#[error]`")
})?;
used_positions.insert(index);
format!("__arg_{index}")
} else {
used_names.insert(arg.clone());
arg
};
out.push('{');
out.push_str(&reference);
out.push_str(&spec);
out.push('}');
}
other => out.push(other),
}
}
Ok(FormatSpec {
original: lit.clone(),
rewritten: out,
used_names,
used_positions,
})
}
fn read_placeholder(
lit: &LitStr,
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> syn::Result<(String, String)> {
let mut arg = String::new();
let mut spec = String::new();
let mut in_spec = false;
loop {
match chars.next() {
Some('}') => return Ok((arg, spec)),
Some(':') if !in_spec => {
in_spec = true;
spec.push(':');
}
Some(other) => {
if in_spec {
spec.push(other);
} else {
arg.push(other);
}
}
None => {
return Err(syn::Error::new(
lit.span(),
"unterminated `{` in `#[error]` format string",
));
}
}
}
}