use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::ext::IdentExt as _;
use syn::{GenericParam, Generics, Ident, Type, Visibility};
use crate::utils::pretty::Pretty as _;
use super::generics::{DeclaredParams, ReferencedParams};
use super::model::{ResolvedEnum, ResolvedStruct};
use super::parse::{CategorizedFields, ModuleSetting, SourceKind, SuffixSetting, UserField};
struct SelectorShape<'a> {
generics: &'a Generics,
struct_decl: TokenStream2,
struct_use: TokenStream2,
selector_params: Vec<GenericParam>,
into_bounds: Vec<TokenStream2>,
referenced_names: std::collections::HashSet<String>,
struct_fields: TokenStream2,
derives: TokenStream2,
}
fn selector_shape<'a>(
user_fields: &[UserField],
generics: &'a Generics,
doc: &dyn Fn(&UserField) -> String,
) -> SelectorShape<'a> {
let declared = DeclaredParams::from_generics(generics);
let mut referenced = ReferencedParams::default();
let mut into_param_idents = Vec::new();
let mut into_bounds = Vec::new();
let mut fields = Vec::new();
let mut all_into_params = true;
for (i, uf) in user_fields.iter().enumerate() {
let field_ident = &uf.ident;
let field_ty = &uf.ty;
let cfg = &uf.cfg_attrs;
let field_doc = doc(uf);
referenced.add_type(field_ty, &declared);
let references_param = declared.type_references_param(field_ty);
if cfg.is_empty() && !references_param {
let ty_param = format_ident!("__T{}", i);
into_param_idents.push(ty_param.clone());
into_bounds.push(quote! { #ty_param: ::core::convert::Into<#field_ty> });
fields.push(quote! { #[doc = #field_doc] pub #field_ident: #ty_param });
} else {
all_into_params = false;
fields.push(quote! { #(#cfg)* #[doc = #field_doc] pub #field_ident: #field_ty });
}
}
let projected = super::generics::project(generics, &referenced);
let into_param_tokens: Vec<TokenStream2> =
into_param_idents.iter().map(|p| quote! { #p }).collect();
let struct_decl = join_generics(projected.decl(), &into_param_tokens);
let struct_use = join_generics(projected.use_(), &into_param_tokens);
let mut selector_params = projected.params().to_vec();
for ident in &into_param_idents {
selector_params.push(syn::parse_quote! { #ident });
}
let referenced_names = projected
.params()
.iter()
.map(super::generics::param_name)
.collect();
let derives = if all_into_params {
quote! { #[derive(Debug, Copy, Clone)] }
} else {
quote! { #[derive(Debug)] }
};
SelectorShape {
generics,
struct_decl,
struct_use,
selector_params,
into_bounds,
referenced_names,
struct_fields: quote! { { #(#fields),* } },
derives,
}
}
impl SelectorShape<'_> {
fn selector_impl(&self) -> (TokenStream2, TokenStream2) {
let params = &self.selector_params;
let impl_generics = if params.is_empty() {
quote! {}
} else {
quote! { <#(#params),*> }
};
let mut predicates = self.scoped_predicates(&self.referenced_names);
predicates.extend(self.into_bounds.iter().cloned());
(impl_generics, render_where(&predicates))
}
fn sourced_impl(&self) -> (TokenStream2, TokenStream2) {
let mut params: Vec<GenericParam> = self.generics.params.iter().cloned().collect();
for param in &self.selector_params {
if super::generics::param_name(param).starts_with("__T") {
params.push(param.clone());
}
}
let impl_generics = if params.is_empty() {
quote! {}
} else {
quote! { <#(#params),*> }
};
let mut predicates: Vec<TokenStream2> = self
.generics
.where_clause
.iter()
.flat_map(|wc| wc.predicates.iter())
.map(|pred| quote! { #pred })
.collect();
predicates.extend(self.into_bounds.iter().cloned());
(impl_generics, render_where(&predicates))
}
fn unconstrained_error_param(&self, source_type: &Type) -> Option<&GenericParam> {
let declared = DeclaredParams::from_generics(self.generics);
let mut from_source = ReferencedParams::default();
from_source.add_type(source_type, &declared);
let constrained = &self.referenced_names | &from_source.names();
self.generics.params.iter().find(|param| match param {
GenericParam::Type(_) | GenericParam::Const(_) => {
!constrained.contains(&super::generics::param_name(param))
}
GenericParam::Lifetime(_) => false,
})
}
fn leaf_predicate_pool(&self) -> Vec<syn::WherePredicate> {
let mut pool =
super::generics::projected_bound_predicates(self.generics, &self.referenced_names);
pool.extend(
self.generics
.where_clause
.iter()
.flat_map(|wc| wc.predicates.iter())
.cloned(),
);
pool
}
fn scoped_predicates(&self, names: &std::collections::HashSet<String>) -> Vec<TokenStream2> {
let declared = DeclaredParams::from_generics(self.generics);
self.leaf_predicate_pool()
.iter()
.filter(|pred| {
super::generics::predicate_named_params(pred, &declared).is_subset(names)
})
.map(|pred| quote! { #pred })
.collect()
}
fn free_params(&self) -> (Vec<GenericParam>, Vec<TokenStream2>) {
let decl: Vec<GenericParam> = self
.generics
.params
.iter()
.filter(|p| {
!self
.referenced_names
.contains(&super::generics::param_name(p))
})
.cloned()
.collect();
let free_names: std::collections::HashSet<String> =
decl.iter().map(super::generics::param_name).collect();
let declared = DeclaredParams::from_generics(self.generics);
let predicates = self
.leaf_predicate_pool()
.iter()
.filter(|pred| {
!super::generics::predicate_named_params(pred, &declared).is_disjoint(&free_names)
})
.map(|pred| quote! { #pred })
.collect();
(decl, predicates)
}
}
fn join_generics(base: TokenStream2, extra: &[TokenStream2]) -> TokenStream2 {
let base_empty = base.is_empty();
match (base_empty, extra.is_empty()) {
(true, true) => quote! {},
(true, false) => quote! { <#(#extra),*> },
(false, true) => base,
(false, false) => {
let inner = strip_angle_brackets(&base);
quote! { <#inner, #(#extra),*> }
}
}
}
fn strip_angle_brackets(generics: &TokenStream2) -> TokenStream2 {
let mut iter = generics.clone().into_iter();
iter.next();
let mut collected: Vec<proc_macro2::TokenTree> = iter.collect();
collected.pop();
collected.into_iter().collect()
}
fn render_where(predicates: &[TokenStream2]) -> TokenStream2 {
if predicates.is_empty() {
quote! {}
} else {
quote! { where #(#predicates),* }
}
}
fn user_init_exprs(user_fields: &[UserField], generics: &syn::Generics) -> Vec<TokenStream2> {
let declared = DeclaredParams::from_generics(generics);
user_fields
.iter()
.map(|uf| {
let ident = &uf.ident;
let cfg = &uf.cfg_attrs;
if cfg.is_empty() && !declared.type_references_param(&uf.ty) {
quote! { #ident: self.#ident.into() }
} else {
quote! { #(#cfg)* #ident: self.#ident }
}
})
.collect()
}
fn resolve_selector_vis(
explicit: Option<&Visibility>,
error_vis: &Visibility,
wrapped_in_module: bool,
) -> Visibility {
let manifest_vis = crate::utils::manifest_vis_default();
let chosen = explicit.or(manifest_vis.as_ref()).unwrap_or(error_vis);
if wrapped_in_module {
lift_into_child_module(chosen)
} else {
chosen.clone()
}
}
fn lift_into_child_module(vis: &Visibility) -> Visibility {
let restricted = match vis {
Visibility::Public(_) => return vis.clone(),
Visibility::Inherited => return syn::parse_quote! { pub(super) },
Visibility::Restricted(restricted) => restricted,
};
let path = &restricted.path;
match path.segments.first() {
Some(seg) if seg.ident == "crate" => vis.clone(),
Some(seg) if seg.ident == "self" && path.segments.len() == 1 => {
syn::parse_quote! { pub(super) }
}
_ => syn::parse_quote! { pub(in super::#path) },
}
}
pub fn gen_enum_selectors(
resolved: &ResolvedEnum,
oopsie_path: &syn::Path,
) -> syn::Result<Vec<TokenStream2>> {
let input = resolved.input;
let container = resolved.container;
let enum_ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let wrapped_in_module = matches!(container.effective_module(true), ModuleSetting::On(_));
let vis = resolve_selector_vis(container.visibility(), &input.vis, wrapped_in_module);
let mut selectors = Vec::new();
for v in &resolved.variants {
let variant_attrs = &v.attrs;
let categorized = &v.fields;
let variant_ident = v.ident();
let cfg_attrs = &v.cfg_attrs;
let Some(selector_ident) = &v.selector_ident else {
let source = categorized
.source
.as_ref()
.expect("transparent variant has a validated source");
let source_ident = &source.ident;
let (param_ty, body_assign) = match &source.kind {
SourceKind::Transformed {
source_type,
transform,
} => (
quote! { #source_type },
quote! { let #source_ident = (#transform)(source); },
),
SourceKind::AutoBoxed { source_type } => (
quote! { #source_type },
quote! { let #source_ident = #oopsie_path::__private::alloc::boxed::Box::new(source); },
),
SourceKind::Yes => {
let ty = &source.ty;
(quote! { #ty }, quote! { let #source_ident = source; })
}
SourceKind::No | SourceKind::Disabled => {
unreachable!("categorized.source set but kind is SourceKind::No or Disabled")
}
};
let auto_inits = gen_auto_inits(categorized, oopsie_path, true);
let auto_names = gen_auto_field_inits(categorized);
let doc = format!("Converts `{param_ty}` into `{enum_ident}::{variant_ident}`.");
selectors.push(quote! {
#(#cfg_attrs)*
impl #impl_generics ::core::convert::From<#param_ty> for #enum_ident #ty_generics #where_clause {
#[doc = #doc]
#[track_caller]
fn from(source: #param_ty) -> Self {
#(#auto_inits)*
#body_assign
#enum_ident::#variant_ident {
#source_ident,
#(#auto_names)*
}
}
}
});
continue;
};
let selector_vis = match variant_attrs.visibility() {
Some(explicit) => resolve_selector_vis(Some(explicit), &vis, wrapped_in_module),
None => vis.clone(),
};
let has_source = categorized.source.is_some();
let user_fields = &categorized.user_fields;
let shape = selector_shape(user_fields, &input.generics, &|uf| {
format!(
"Value for the `{}` field of `{enum_ident}::{variant_ident}`.\n\n\
Field type: `{}`",
uf.ident,
uf.ty.pretty()
)
});
let SelectorShape {
struct_decl,
struct_fields,
derives,
..
} = &shape;
let selector_doc = format!("Context selector for `{enum_ident}::{variant_ident}`.");
let selector_struct = if user_fields.is_empty() {
quote! {
#(#cfg_attrs)*
#[doc = #selector_doc]
#[derive(Debug, Copy, Clone)]
#selector_vis struct #selector_ident;
}
} else {
quote! {
#(#cfg_attrs)*
#[doc = #selector_doc]
#derives
#selector_vis struct #selector_ident #struct_decl #struct_fields
}
};
let dest_ty = quote! { #enum_ident #ty_generics };
let construct = quote! { #enum_ident::#variant_ident };
let doc_name = format!("{enum_ident}::{}", variant_ident.unraw());
let dest = Destination {
ty: &dest_ty,
construct: &construct,
doc_name: &doc_name,
generics: &input.generics,
};
let methods_inner = if has_source {
reject_unconstrained_sourced(categorized, &shape)?;
gen_build_error(selector_ident, &dest, categorized, &shape, oopsie_path)
} else {
gen_build_fail(selector_ident, &dest, categorized, &shape, oopsie_path)
};
let methods = if cfg_attrs.is_empty() {
methods_inner
} else {
quote! {
#(#cfg_attrs)*
const _: () = {
#methods_inner
};
}
};
selectors.push(quote! {
#selector_struct
#methods
});
}
Ok(selectors)
}
pub fn gen_struct_selector(
resolved: &ResolvedStruct,
oopsie_path: &syn::Path,
) -> syn::Result<TokenStream2> {
let input = resolved.input;
let attrs = resolved.attrs;
let struct_ident = &input.ident;
let (_, ty_generics, _) = input.generics.split_for_impl();
let wrapped_in_module = matches!(
attrs.container.effective_module(false),
ModuleSetting::On(_)
);
let vis = resolve_selector_vis(attrs.visibility(), &input.vis, wrapped_in_module);
let dest_ty: TokenStream2 = if wrapped_in_module {
quote! { super::#struct_ident #ty_generics }
} else {
quote! { #struct_ident #ty_generics }
};
let construct: TokenStream2 = if wrapped_in_module {
quote! { super::#struct_ident }
} else {
quote! { #struct_ident }
};
let variant_attrs = attrs;
let categorized = &resolved.fields;
if variant_attrs.transparent {
let source = categorized
.source
.as_ref()
.expect("transparent struct has a validated source");
let source_ident = &source.ident;
let (param_ty, body_assign) = match &source.kind {
SourceKind::Transformed {
source_type,
transform,
} => (
quote! { #source_type },
quote! { let #source_ident = (#transform)(source); },
),
SourceKind::AutoBoxed { source_type } => (
quote! { #source_type },
quote! { let #source_ident = #oopsie_path::__private::alloc::boxed::Box::new(source); },
),
SourceKind::Yes => {
let ty = &source.ty;
(quote! { #ty }, quote! { let #source_ident = source; })
}
SourceKind::No | SourceKind::Disabled => {
unreachable!("categorized.source set but kind is SourceKind::No or Disabled")
}
};
let auto_inits = gen_auto_inits(categorized, oopsie_path, true);
let auto_names = gen_auto_field_inits(categorized);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let doc = format!("Converts `{param_ty}` into `{struct_ident}`.");
return Ok(quote! {
impl #impl_generics ::core::convert::From<#param_ty> for #struct_ident #ty_generics #where_clause {
#[doc = #doc]
#[track_caller]
fn from(source: #param_ty) -> Self {
#(#auto_inits)*
#body_assign
Self { #source_ident, #(#auto_names)* }
}
}
});
}
let selector_ident = selector_name(struct_ident, &attrs.container.effective_suffix(false))?;
let has_source = categorized.source.is_some();
let user_fields = &categorized.user_fields;
let shape = selector_shape(user_fields, &input.generics, &|uf| {
format!(
"Value for the `{}` field of `{struct_ident}`.\n\n\
Field type: `{}`",
uf.ident,
uf.ty.pretty()
)
});
let SelectorShape {
struct_decl,
struct_fields,
derives,
..
} = &shape;
let selector_doc = format!("Context selector for `{struct_ident}`.");
let selector_struct = if user_fields.is_empty() {
quote! {
#[doc = #selector_doc]
#[derive(Debug, Copy, Clone)]
#vis struct #selector_ident;
}
} else {
quote! {
#[doc = #selector_doc]
#derives
#vis struct #selector_ident #struct_decl #struct_fields
}
};
let doc_name = struct_ident.unraw().to_string();
let dest = Destination {
ty: &dest_ty,
construct: &construct,
doc_name: &doc_name,
generics: &input.generics,
};
let methods = if has_source {
reject_unconstrained_sourced(categorized, &shape)?;
gen_build_error(&selector_ident, &dest, categorized, &shape, oopsie_path)
} else {
gen_build_fail(&selector_ident, &dest, categorized, &shape, oopsie_path)
};
Ok(quote! {
#selector_struct
#methods
})
}
pub(super) fn selector_name(base: &Ident, suffix: &SuffixSetting) -> syn::Result<Ident> {
let base_str = base.unraw().to_string();
let stripped = base_str
.strip_suffix("Error")
.filter(|s| !s.is_empty())
.unwrap_or(&base_str);
let name = match suffix {
SuffixSetting::Off => stripped.to_owned(),
SuffixSetting::Custom(s) => format!("{stripped}{s}"),
};
ident_maybe_raw(&name, base.span())
}
fn ident_maybe_raw(name: &str, span: proc_macro2::Span) -> syn::Result<Ident> {
match syn::parse_str::<Ident>(name) {
Ok(mut id) => {
id.set_span(span);
Ok(id)
}
Err(_) if matches!(name, "self" | "Self" | "super" | "crate" | "_") => {
Err(syn::Error::new(
span,
format!(
"cannot generate a selector named `{name}`; rename the item or set \
`#[oopsie(suffix = \"...\")]`"
),
))
}
Err(_) => Ok(Ident::new_raw(name, span)),
}
}
fn gen_auto_inits(
categorized: &CategorizedFields,
oopsie_path: &syn::Path,
has_source: bool,
) -> Vec<TokenStream2> {
categorized
.auto_fields
.iter()
.map(|af| {
let ident = &af.ident;
let ty = &af.ty;
let cfg = &af.cfg_attrs;
if has_source {
quote! {
#(#cfg)*
let #ident = {
use #oopsie_path::__private::{CaptureFromExt as _, CaptureFromFallback as _};
(&#oopsie_path::__private::CaptureProbe(&source)).resolve::<#ty>()
};
}
} else {
quote! { #(#cfg)* let #ident = <#ty as #oopsie_path::Capturable>::capture(); }
}
})
.collect()
}
fn gen_auto_field_inits(categorized: &CategorizedFields) -> Vec<TokenStream2> {
categorized
.auto_fields
.iter()
.map(|af| {
let ident = &af.ident;
let cfg = &af.cfg_attrs;
quote! { #(#cfg)* #ident, }
})
.collect()
}
struct Destination<'a> {
ty: &'a TokenStream2,
construct: &'a TokenStream2,
doc_name: &'a str,
generics: &'a syn::Generics,
}
fn sourced_impl_source_type(source_field: &super::parse::SourceField) -> &Type {
match &source_field.kind {
SourceKind::No | SourceKind::Disabled => {
unreachable!("categorized.source set but kind is SourceKind::No or Disabled")
}
SourceKind::Yes => &source_field.ty,
SourceKind::Transformed { source_type, .. } | SourceKind::AutoBoxed { source_type } => {
source_type
}
}
}
fn reject_unconstrained_sourced(
categorized: &CategorizedFields,
shape: &SelectorShape,
) -> syn::Result<()> {
let source_field = categorized
.source
.as_ref()
.expect("sourced selector has a source field");
if let Some(param) = shape.unconstrained_error_param(sourced_impl_source_type(source_field)) {
let name = super::generics::param_name(param);
return Err(syn::Error::new_spanned(
param,
format!(
"a variant with a `source` field must reference every generic parameter of the \
error type in its source type or its other fields; parameter `{name}` is used \
only by other variants, which would leave this selector's `Contextual` impl \
unconstrained — split it into its own error type or add `{name}` to this variant"
),
));
}
Ok(())
}
fn gen_build_error(
selector_ident: &Ident,
dest: &Destination,
categorized: &CategorizedFields,
shape: &SelectorShape,
oopsie_path: &syn::Path,
) -> TokenStream2 {
let dest_ty = dest.ty;
let construct = dest.construct;
let generics = dest.generics;
let source_field = categorized.source.as_ref().unwrap();
let source_ident = &source_field.ident;
let (source_type, source_transform) = match &source_field.kind {
SourceKind::No | SourceKind::Disabled => {
unreachable!("categorized.source set but kind is SourceKind::No or Disabled")
}
SourceKind::Yes => {
let ty = &source_field.ty;
(quote! { #ty }, None)
}
SourceKind::Transformed {
source_type,
transform,
} => (quote! { #source_type }, Some(quote! { (#transform) })),
SourceKind::AutoBoxed { source_type } => (
quote! { #source_type },
Some(quote! { #oopsie_path::__private::alloc::boxed::Box::new }),
),
};
let auto_inits = gen_auto_inits(categorized, oopsie_path, true);
let auto_names = gen_auto_field_inits(categorized);
let user_inits = user_init_exprs(&categorized.user_fields, generics);
let source_assign = if let Some(transform) = source_transform {
quote! { let #source_ident = #transform(source); }
} else {
quote! { let #source_ident = source; }
};
let struct_use = &shape.struct_use;
let (impl_generics, impl_where) = shape.sourced_impl();
quote! {
impl #impl_generics #oopsie_path::Contextual<#source_type> for #selector_ident #struct_use
#impl_where
{
type Destination = #dest_ty;
#[track_caller]
fn build_error(self, source: #source_type) -> #dest_ty {
#(#auto_inits)*
#source_assign
#construct {
#(#user_inits,)*
#source_ident,
#(#auto_names)*
}
}
}
}
}
fn gen_build_fail(
selector_ident: &Ident,
dest: &Destination,
categorized: &CategorizedFields,
shape: &SelectorShape,
oopsie_path: &syn::Path,
) -> TokenStream2 {
let dest_ty = dest.ty;
let construct = dest.construct;
let doc_name = dest.doc_name;
let generics = dest.generics;
let auto_inits = gen_auto_inits(categorized, oopsie_path, false);
let auto_names = gen_auto_field_inits(categorized);
let user_inits = user_init_exprs(&categorized.user_fields, generics);
let struct_use = &shape.struct_use;
let (impl_generics, impl_where) = shape.selector_impl();
let (free_decl, free_preds) = shape.free_params();
let build_generics = if free_decl.is_empty() {
quote! {}
} else {
quote! { <#(#free_decl),*> }
};
let build_where = render_where(&free_preds);
let fail_generics = if free_decl.is_empty() {
quote! { <__T> }
} else {
let (free_lts, free_rest): (Vec<_>, Vec<_>) = free_decl
.iter()
.partition(|p| matches!(p, GenericParam::Lifetime(_)));
quote! { <#(#free_lts,)* __T, #(#free_rest),*> }
};
let fail_where = render_where(&free_preds);
let none_error_impl = if free_decl.is_empty() {
quote! {
impl #impl_generics #oopsie_path::Contextual<#oopsie_path::NoSource> for #selector_ident #struct_use
#impl_where
{
type Destination = #dest_ty;
#[track_caller]
fn build_error(self, _: #oopsie_path::NoSource) -> #dest_ty {
self.build()
}
}
}
} else {
quote! {}
};
let build_doc = format!("Builds `{doc_name}` from this selector's fields.");
let fail_doc = format!("Builds `{doc_name}` and returns it as `Err`.");
quote! {
impl #impl_generics #selector_ident #struct_use
#impl_where
{
#[doc = #build_doc]
#[must_use]
#[track_caller]
pub fn build #build_generics (self) -> #dest_ty #build_where {
#(#auto_inits)*
#construct {
#(#user_inits,)*
#(#auto_names)*
}
}
#[doc = #fail_doc]
#[track_caller]
pub fn fail #fail_generics (self) -> ::core::result::Result<__T, #dest_ty> #fail_where {
::core::result::Result::Err(self.build())
}
}
#none_error_impl
}
}