use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{DeriveInput, Ident};
use super::parse::{CategorizedFields, EnumContainerAttrs, StructAttrs, VariantAttrs};
pub struct ResolvedVariant<'a> {
pub variant: &'a syn::Variant,
pub attrs: VariantAttrs,
pub fields: CategorizedFields,
pub cfg_attrs: Vec<syn::Attribute>,
pub selector_ident: Option<Ident>,
}
impl ResolvedVariant<'_> {
pub const fn ident(&self) -> &Ident {
&self.variant.ident
}
}
pub struct ResolvedEnum<'a> {
pub input: &'a DeriveInput,
pub container: &'a EnumContainerAttrs,
pub variants: Vec<ResolvedVariant<'a>>,
pub any_variant_cfg: bool,
}
pub struct ResolvedStruct<'a> {
pub input: &'a DeriveInput,
pub attrs: &'a StructAttrs,
pub fields: CategorizedFields,
}
impl<'a> ResolvedEnum<'a> {
pub fn resolve(input: &'a DeriveInput, container: &'a EnumContainerAttrs) -> syn::Result<Self> {
let syn::Data::Enum(data) = &input.data else {
unreachable!("ResolvedEnum::resolve called on a non-enum")
};
let mut categorized: Vec<CategorizedFields> = Vec::with_capacity(data.variants.len());
for variant in &data.variants {
categorized.push(CategorizedFields::from_fields(&variant.fields)?);
}
for fields in &categorized {
reject_cfg_on_source(fields)?;
}
let suffix = container.effective_suffix(true);
let mut seen_selectors: std::collections::HashMap<String, Ident> =
std::collections::HashMap::new();
let mut variants = Vec::with_capacity(data.variants.len());
for (variant, fields) in data.variants.iter().zip(categorized) {
let attrs = VariantAttrs::from_attrs(&variant.attrs)?;
let cfg_attrs: Vec<syn::Attribute> = variant
.attrs
.iter()
.filter(|a| a.path().is_ident("cfg"))
.cloned()
.collect();
let selector_ident = if attrs.transparent {
validate_transparent(&variant.ident, &fields)?;
None
} else {
let ident = super::gen_selectors::selector_name(&variant.ident, &suffix)?;
if cfg_attrs.is_empty()
&& let Some(first) =
seen_selectors.insert(ident.to_string(), variant.ident.clone())
{
return Err(selector_collision_error(&first, &variant.ident, &ident));
}
Some(ident)
};
variants.push(ResolvedVariant {
variant,
attrs,
fields,
cfg_attrs,
selector_ident,
});
}
for v in &variants {
reject_display_keywords(v.variant, &v.attrs)?;
}
for v in &variants {
validate_help_conflict(&v.fields, &v.attrs)?;
}
Ok(Self {
input,
container,
variants,
any_variant_cfg: super::parse::any_variant_has_cfg(data),
})
}
pub fn reject_inert_variant_traced(&self) -> syn::Result<()> {
const MSG: &str = "`traced` on an enum variant only applies when the enum is traced \
with `#[oopsie::oopsie(traced)]`; add `traced` to the enum or remove this";
for v in &self.variants {
if v.attrs.traced.to_option().is_none() {
continue;
}
return match v.variant.attrs.iter().find(|a| attr_mentions_traced(a)) {
Some(attr) => Err(syn::Error::new_spanned(attr, MSG)),
None => Err(syn::Error::new_spanned(v.ident(), MSG)),
};
}
Ok(())
}
}
fn attr_mentions_traced(attr: &syn::Attribute) -> bool {
attr.path().is_ident("oopsie")
&& attr
.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
)
.is_ok_and(|metas| metas.iter().any(|m| m.path().is_ident("traced")))
}
impl<'a> ResolvedStruct<'a> {
pub fn resolve(input: &'a DeriveInput, attrs: &'a StructAttrs) -> syn::Result<Self> {
let syn::Data::Struct(data) = &input.data else {
unreachable!("ResolvedStruct::resolve called on a non-struct")
};
let fields = CategorizedFields::from_fields(&data.fields)?;
reject_cfg_on_source(&fields)?;
if attrs.transparent {
validate_transparent(&input.ident, &fields)?;
} else {
let _ = selector_name_for_struct(input, attrs)?;
}
reject_display_keywords_struct(input, attrs)?;
validate_help_conflict(&fields, attrs)?;
Ok(Self {
input,
attrs,
fields,
})
}
}
fn reject_cfg_on_source(fields: &CategorizedFields) -> syn::Result<()> {
if let Some(source) = &fields.source
&& let Some(cfg) = source.cfg_attrs.first()
{
return Err(syn::Error::new_spanned(
cfg,
"`#[cfg(...)]` is not supported on a source field; gating the \
source would strip the `From`/`Contextual` impls that depend on it",
));
}
Ok(())
}
fn validate_transparent(
no_source_span: &dyn quote::ToTokens,
fields: &CategorizedFields,
) -> syn::Result<()> {
if fields.source.is_none() {
return Err(syn::Error::new_spanned(
no_source_span,
"`transparent` requires a source field (a field named `source` \
or marked `#[oopsie(from)]`)",
));
}
if let Some(src) = &fields.source
&& src.forward.any()
{
return Err(syn::Error::new_spanned(
&src.ident,
"`#[oopsie(forward(...))]` is redundant on a `transparent` error; transparent already forwards all traces",
));
}
if let Some(extra) = fields.user_fields.first() {
return Err(syn::Error::new_spanned(
&extra.ident,
"`transparent` allows no fields besides the source and \
auto-captured trace fields; remove this field or drop `transparent`",
));
}
Ok(())
}
fn validate_help_conflict(fields: &CategorizedFields, attrs: &impl HasHelp) -> syn::Result<()> {
if let (Some(help_field), Some(_)) = (&fields.help_field, attrs.help_attr()) {
return Err(syn::Error::new_spanned(
help_field,
"ambiguous help: this `#[oopsie(help)]` field conflicts with the \
`help = ...` attribute; remove one",
));
}
Ok(())
}
fn reject_display_keywords(variant: &syn::Variant, attrs: &VariantAttrs) -> syn::Result<()> {
if let Some(display) = &attrs.display {
display.reject_keyword_args(&variant.fields, super::parse::DisplayScope::Variant)?;
}
Ok(())
}
fn reject_display_keywords_struct(input: &DeriveInput, attrs: &StructAttrs) -> syn::Result<()> {
let syn::Data::Struct(data) = &input.data else {
unreachable!()
};
if let Some(display) = &attrs.display {
display.reject_keyword_args(&data.fields, super::parse::DisplayScope::Struct)?;
}
Ok(())
}
fn selector_name_for_struct(input: &DeriveInput, attrs: &StructAttrs) -> syn::Result<Ident> {
super::gen_selectors::selector_name(&input.ident, &attrs.container.effective_suffix(false))
}
fn selector_collision_error(first: &Ident, second: &Ident, selector: &Ident) -> syn::Error {
let mut err = syn::Error::new_spanned(
second,
format!(
"variants `{first}` and `{second}` both generate a selector named \
`{selector}` (a trailing `Error` is stripped from variant names); \
rename one of the variants"
),
);
err.combine(syn::Error::new_spanned(
first,
format!("`{first}` also generates selector `{selector}`"),
));
err
}
pub trait HasHelp {
fn help_attr(&self) -> Option<&super::parse::DisplayAttr>;
}
impl HasHelp for VariantAttrs {
fn help_attr(&self) -> Option<&super::parse::DisplayAttr> {
self.help.as_ref()
}
}
impl HasHelp for StructAttrs {
fn help_attr(&self) -> Option<&super::parse::DisplayAttr> {
self.help.as_ref()
}
}
pub fn field_binding_pats(fields: &syn::Fields) -> Vec<TokenStream2> {
let syn::Fields::Named(named) = fields else {
return Vec::new();
};
named
.named
.iter()
.filter_map(|f| {
let ident = f.ident.as_ref()?;
let cfg = f
.attrs
.iter()
.filter(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"));
Some(quote! { #(#cfg)* #ident, })
})
.collect()
}