Skip to main content

pseudo_backtrace_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::{format_ident, quote};
4use std::collections::{BTreeMap, BTreeSet};
5use syn::visit::Visit;
6use syn::{
7    Data, DataEnum, DataStruct, DeriveInput, Field, Fields, Generics, Ident, Member,
8    parse_macro_input, spanned::Spanned,
9};
10
11#[proc_macro_derive(StackError, attributes(source, stack_error, location))]
12pub fn derive_stack_error(input: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(input as DeriveInput);
14    match expand(input) {
15        Ok(tokens) => tokens.into(),
16        Err(error) => error.into_compile_error().into(),
17    }
18}
19
20fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
21    let ident = input.ident;
22    let generics = input.generics;
23
24    match input.data {
25        Data::Struct(data) => expand_struct(ident, generics, data),
26        Data::Enum(data) => expand_enum(ident, generics, data),
27        Data::Union(_) => Err(syn::Error::new(
28            Span::call_site(),
29            "StackError cannot be derived for unions",
30        )),
31    }
32}
33
34fn expand_struct(ident: Ident, generics: Generics, data: DataStruct) -> syn::Result<TokenStream2> {
35    let style = match &data.fields {
36        Fields::Named(_) => FieldsStyle::Named,
37        Fields::Unnamed(_) => FieldsStyle::Unnamed,
38        Fields::Unit => {
39            return Err(syn::Error::new(
40                ident.span(),
41                "unit structs do not support #[derive(StackError)]",
42            ));
43        }
44    };
45
46    let fields = collect_fields(&data.fields)?;
47    let allow_name = style.allows_names();
48    let has_explicit_location = fields.iter().any(|f| {
49        f.attrs.is_location || (allow_name && matches!(&f.ident, Some(id) if id == "location"))
50    });
51    let location_index = if has_explicit_location {
52        // Defer to the existing resolver and propagate its specific errors
53        resolve_location(&fields, allow_name, ident.span())?
54    } else {
55        // Try implicit LocatedError-based fallback
56        resolve_location_from_located_source(&fields, allow_name)
57            .or_else(|| single_field_located(&fields))
58            .ok_or_else(|| {
59                syn::Error::new(
60                    ident.span(),
61                    "missing #[location] attribute or field named `location`",
62                )
63            })?
64    };
65    let source = resolve_source(&fields, style.allows_names())?;
66
67    let mut generics = generics;
68    let mut bounds = BoundsTracker::new(&generics);
69    if let Some(info) = &source {
70        bounds.collect(&fields[info.index].ty, info.is_terminal);
71    }
72    bounds.apply(&mut generics);
73
74    let location_member = &fields[location_index].member;
75    let location_expr = if is_located_error(&fields[location_index].ty) {
76        quote! { ::pseudo_backtrace::StackError::location(&self.#location_member) }
77    } else {
78        quote! { self.#location_member }
79    };
80    let next_body = match &source {
81        Some(info) => build_next_struct(
82            &fields[info.index].member,
83            &fields[info.index].ty,
84            info.is_terminal,
85        ),
86        None => quote! { ::core::option::Option::None },
87    };
88
89    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
90
91    Ok(quote! {
92        impl #impl_generics ::pseudo_backtrace::StackError for #ident #ty_generics #where_clause {
93            fn location(&self) -> &'static ::core::panic::Location<'static> {
94                #location_expr
95            }
96
97            fn next<'pseudo_backtrace>(&'pseudo_backtrace self) -> ::core::option::Option<::pseudo_backtrace::Chain<'pseudo_backtrace>> {
98                use ::pseudo_backtrace::private::AsDynStdError as _;
99                use ::pseudo_backtrace::private::AsDynStackError as _;
100                #next_body
101            }
102        }
103    })
104}
105
106fn expand_enum(ident: Ident, generics: Generics, data: DataEnum) -> syn::Result<TokenStream2> {
107    let mut variant_infos = Vec::with_capacity(data.variants.len());
108    let mut errors: Option<syn::Error> = None;
109
110    for variant in data.variants {
111        let style = match &variant.fields {
112            Fields::Named(_) => FieldsStyle::Named,
113            Fields::Unnamed(_) => FieldsStyle::Unnamed,
114            Fields::Unit => {
115                errors = combine_error(
116                    errors,
117                    syn::Error::new(
118                        variant.ident.span(),
119                        "unit variants do not support #[derive(StackError)]",
120                    ),
121                );
122                continue;
123            }
124        };
125
126        let fields = match collect_fields(&variant.fields) {
127            Ok(fields) => fields,
128            Err(err) => {
129                errors = combine_error(errors, err);
130                continue;
131            }
132        };
133
134        let allow_name = style.allows_names();
135        let has_explicit_location = fields.iter().any(|f| {
136            f.attrs.is_location || (allow_name && matches!(&f.ident, Some(id) if id == "location"))
137        });
138        let location_index = if has_explicit_location {
139            match resolve_location(&fields, allow_name, variant.ident.span()) {
140                Ok(index) => Some(index),
141                Err(err) => {
142                    errors = combine_error(errors, err);
143                    None
144                }
145            }
146        } else {
147            resolve_location_from_located_source(&fields, allow_name)
148                .or_else(|| single_field_located(&fields))
149        };
150        let Some(location_index) = location_index else {
151            errors = combine_error(
152                errors,
153                syn::Error::new(
154                    variant.ident.span(),
155                    "missing #[location] attribute or field named `location`",
156                ),
157            );
158            continue;
159        };
160
161        let source = match resolve_source(&fields, style.allows_names()) {
162            Ok(source) => source,
163            Err(err) => {
164                errors = combine_error(errors, err);
165                continue;
166            }
167        };
168
169        let source_binding = source
170            .as_ref()
171            .map(|_| format_ident!("__stack_error_source"));
172
173        variant_infos.push(VariantInfo {
174            ident: variant.ident,
175            style,
176            fields,
177            location_index,
178            source,
179            location_binding: format_ident!("__stack_error_location"),
180            source_binding,
181        });
182    }
183
184    if let Some(err) = errors {
185        return Err(err);
186    }
187
188    let mut generics = generics;
189    let mut bounds = BoundsTracker::new(&generics);
190    for variant in &variant_infos {
191        if let Some(source) = &variant.source {
192            bounds.collect(&variant.fields[source.index].ty, source.is_terminal);
193        }
194    }
195    bounds.apply(&mut generics);
196
197    let location_arms = variant_infos.iter().map(|variant| {
198        let variant_ident = &variant.ident;
199        let pattern = variant.location_pattern();
200        let value = variant.location_value_expr();
201        quote! {
202            Self::#variant_ident #pattern => #value
203        }
204    });
205
206    let next_arms = variant_infos.iter().map(|variant| {
207        let variant_ident = &variant.ident;
208        let pattern = variant.source_pattern();
209        let body = variant.next_body();
210        quote! {
211            Self::#variant_ident #pattern => #body
212        }
213    });
214
215    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
216
217    Ok(quote! {
218        impl #impl_generics ::pseudo_backtrace::StackError for #ident #ty_generics #where_clause {
219            fn location(&self) -> &'static ::core::panic::Location<'static> {
220                match self {
221                    #(#location_arms,)*
222                }
223            }
224
225            fn next<'pseudo_backtrace>(&'pseudo_backtrace self) -> ::core::option::Option<::pseudo_backtrace::Chain<'pseudo_backtrace>> {
226                use ::pseudo_backtrace::private::AsDynStdError as _;
227                use ::pseudo_backtrace::private::AsDynStackError as _;
228                match self {
229                    #(#next_arms,)*
230                }
231            }
232        }
233    })
234}
235
236#[derive(Clone)]
237struct FieldInfo {
238    member: Member,
239    ident: Option<Ident>,
240    ty: syn::Type,
241    attrs: FieldAttrs,
242    span: Span,
243}
244
245#[derive(Clone, Copy)]
246enum FieldsStyle {
247    Named,
248    Unnamed,
249}
250
251impl FieldsStyle {
252    fn allows_names(self) -> bool {
253        matches!(self, FieldsStyle::Named)
254    }
255}
256
257#[derive(Default, Clone)]
258struct FieldAttrs {
259    is_source: bool,
260    is_location: bool,
261    is_terminal: bool,
262}
263
264struct SourceInfo {
265    index: usize,
266    is_terminal: bool,
267}
268
269struct VariantInfo {
270    ident: Ident,
271    style: FieldsStyle,
272    fields: Vec<FieldInfo>,
273    location_index: usize,
274    source: Option<SourceInfo>,
275    location_binding: Ident,
276    source_binding: Option<Ident>,
277}
278
279fn collect_fields(fields: &Fields) -> syn::Result<Vec<FieldInfo>> {
280    let mut out = Vec::new();
281
282    match fields {
283        Fields::Named(named) => {
284            for field in named.named.iter() {
285                out.push(build_field_info(field, out.len(), true)?);
286            }
287        }
288        Fields::Unnamed(unnamed) => {
289            for (idx, field) in unnamed.unnamed.iter().enumerate() {
290                out.push(build_field_info(field, idx, false)?);
291            }
292        }
293        Fields::Unit => {}
294    }
295
296    Ok(out)
297}
298
299fn build_field_info(field: &Field, index: usize, named: bool) -> syn::Result<FieldInfo> {
300    let attrs = parse_field_attrs(field)?;
301    let member = if named {
302        Member::Named(field.ident.clone().expect("named field missing ident"))
303    } else {
304        Member::Unnamed(syn::Index::from(index))
305    };
306
307    Ok(FieldInfo {
308        member,
309        ident: field.ident.clone(),
310        ty: field.ty.clone(),
311        attrs,
312        span: field.span(),
313    })
314}
315
316fn parse_field_attrs(field: &Field) -> syn::Result<FieldAttrs> {
317    let mut attrs = FieldAttrs::default();
318
319    for attr in &field.attrs {
320        if attr.path().is_ident("source") {
321            if attrs.is_source {
322                return Err(syn::Error::new_spanned(
323                    attr,
324                    "duplicate #[source] attribute",
325                ));
326            }
327            attrs.is_source = true;
328            continue;
329        }
330
331        if attr.path().is_ident("location") {
332            if attrs.is_location {
333                return Err(syn::Error::new_spanned(
334                    attr,
335                    "duplicate #[location] attribute",
336                ));
337            }
338            attrs.is_location = true;
339            continue;
340        }
341
342        if attr.path().is_ident("stack_error") {
343            match attr.parse_args_with(|input: syn::parse::ParseStream| {
344                let ident: Ident = input.parse()?;
345                if ident == "std" {
346                    Ok((true, ident.span()))
347                } else if ident == "stacked" {
348                    Ok((false, ident.span()))
349                } else {
350                    Err(syn::Error::new(ident.span(), "expected `std` or `stacked`"))
351                }
352            }) {
353                Ok((is_std, _span)) => {
354                    if is_std {
355                        if attrs.is_terminal {
356                            return Err(syn::Error::new_spanned(
357                                attr,
358                                "duplicate #[stack_error(std)] attribute",
359                            ));
360                        }
361                        attrs.is_terminal = true;
362                    } else {
363                        if attrs.is_source {
364                            return Err(syn::Error::new_spanned(
365                                attr,
366                                "duplicate #[stack_error(stacked)] attribute",
367                            ));
368                        }
369                        attrs.is_source = true;
370                    }
371                }
372                Err(err) => {
373                    return Err(syn::Error::new_spanned(
374                        attr,
375                        format!("invalid #[stack_error] attribute: {}", err),
376                    ));
377                }
378            }
379
380            continue;
381        }
382    }
383
384    Ok(attrs)
385}
386
387fn resolve_location(
388    fields: &[FieldInfo],
389    allow_name: bool,
390    missing_span: Span,
391) -> syn::Result<usize> {
392    let mut index = None;
393
394    for (idx, field) in fields.iter().enumerate() {
395        if field.attrs.is_location {
396            if index.is_some() {
397                return Err(syn::Error::new(
398                    field.span,
399                    "multiple fields marked with #[location]",
400                ));
401            }
402            index = Some(idx);
403        }
404    }
405
406    if let Some(idx) = index {
407        return Ok(idx);
408    }
409
410    if allow_name
411        && let Some((idx, _)) = fields
412            .iter()
413            .enumerate()
414            .find(|(_, field)| matches!(&field.ident, Some(ident) if ident == "location"))
415    {
416        return Ok(idx);
417    }
418
419    Err(syn::Error::new(
420        missing_span,
421        "missing #[location] attribute or field named `location`",
422    ))
423}
424
425fn resolve_source(fields: &[FieldInfo], allow_name: bool) -> syn::Result<Option<SourceInfo>> {
426    let mut source_candidates: Vec<usize> = Vec::new();
427    let mut terminal_candidates: Vec<usize> = Vec::new();
428
429    for (idx, field) in fields.iter().enumerate() {
430        if field.attrs.is_source {
431            source_candidates.push(idx);
432        }
433        if field.attrs.is_terminal {
434            terminal_candidates.push(idx);
435        }
436    }
437
438    if source_candidates.len() > 1 {
439        let span = fields[source_candidates[1]].span;
440        return Err(syn::Error::new(
441            span,
442            "multiple fields marked with #[source]",
443        ));
444    }
445
446    if source_candidates.len() == 1 {
447        let idx = source_candidates[0];
448        let is_terminal = fields[idx].attrs.is_terminal;
449        return Ok(Some(SourceInfo {
450            index: idx,
451            is_terminal,
452        }));
453    }
454
455    if terminal_candidates.len() > 1 {
456        let span = fields[terminal_candidates[1]].span;
457        return Err(syn::Error::new(
458            span,
459            "multiple fields marked with #[stack_error(std)]",
460        ));
461    }
462
463    if let Some(idx) = terminal_candidates.first().copied() {
464        return Ok(Some(SourceInfo {
465            index: idx,
466            is_terminal: true,
467        }));
468    }
469
470    if allow_name
471        && let Some((idx, _)) = fields
472            .iter()
473            .enumerate()
474            .find(|(_, field)| matches!(&field.ident, Some(ident) if ident == "source"))
475    {
476        return Ok(Some(SourceInfo {
477            index: idx,
478            is_terminal: false,
479        }));
480    }
481
482    Ok(None)
483}
484
485fn build_next_struct(member: &Member, ty: &syn::Type, is_terminal: bool) -> TokenStream2 {
486    if is_terminal {
487        if type_parameter_of_option(ty).is_some() {
488            quote! {
489                self.#member
490                    .as_ref()
491                    .map(|__s| ::pseudo_backtrace::Chain::Std(__s.as_dyn_std_error()))
492            }
493        } else {
494            quote! {
495                ::core::option::Option::Some(::pseudo_backtrace::Chain::Std(
496                    self.#member.as_dyn_std_error(),
497                ))
498            }
499        }
500    } else if type_parameter_of_option(ty).is_some() {
501        quote! {
502            self.#member
503                .as_ref()
504                .map(|__s| ::pseudo_backtrace::Chain::Stacked(__s.as_dyn_stack_error()))
505        }
506    } else {
507        quote! {
508            ::core::option::Option::Some(::pseudo_backtrace::Chain::Stacked(
509                self.#member.as_dyn_stack_error(),
510            ))
511        }
512    }
513}
514
515impl VariantInfo {
516    fn location_pattern(&self) -> TokenStream2 {
517        match self.style {
518            FieldsStyle::Named => {
519                let field_ident = self.fields[self.location_index]
520                    .ident
521                    .as_ref()
522                    .expect("named field missing ident")
523                    .clone();
524                let binding = &self.location_binding;
525                quote! { { #field_ident: #binding, .. } }
526            }
527            FieldsStyle::Unnamed => {
528                let binding = &self.location_binding;
529                let patterns = self.fields.iter().enumerate().map(|(idx, _)| {
530                    if idx == self.location_index {
531                        quote! { #binding }
532                    } else {
533                        quote! { _ }
534                    }
535                });
536                quote! { ( #(#patterns),* ) }
537            }
538        }
539    }
540
541    fn location_value_expr(&self) -> TokenStream2 {
542        let binding = &self.location_binding;
543        let ty = &self.fields[self.location_index].ty;
544        if is_located_error(ty) {
545            quote! { ::pseudo_backtrace::StackError::location(#binding) }
546        } else {
547            quote! { #binding }
548        }
549    }
550
551    fn source_pattern(&self) -> TokenStream2 {
552        match &self.source {
553            Some(source) => match self.style {
554                FieldsStyle::Named => {
555                    let field_ident = self.fields[source.index]
556                        .ident
557                        .as_ref()
558                        .expect("named field missing ident")
559                        .clone();
560                    let binding = self
561                        .source_binding
562                        .as_ref()
563                        .expect("source binding missing");
564                    quote! { { #field_ident: #binding, .. } }
565                }
566                FieldsStyle::Unnamed => {
567                    let binding = self
568                        .source_binding
569                        .as_ref()
570                        .expect("source binding missing");
571                    let patterns = self.fields.iter().enumerate().map(|(idx, _)| {
572                        if idx == source.index {
573                            quote! { #binding }
574                        } else {
575                            quote! { _ }
576                        }
577                    });
578                    quote! { ( #(#patterns),* ) }
579                }
580            },
581            None => match self.style {
582                FieldsStyle::Named => quote! { { .. } },
583                FieldsStyle::Unnamed => {
584                    let patterns = self.fields.iter().map(|_| quote! { _ });
585                    quote! { ( #(#patterns),* ) }
586                }
587            },
588        }
589    }
590
591    fn next_body(&self) -> TokenStream2 {
592        match &self.source {
593            Some(source) => {
594                let binding = self
595                    .source_binding
596                    .as_ref()
597                    .expect("source binding missing");
598                let ty = &self.fields[source.index].ty;
599                if source.is_terminal {
600                    if type_parameter_of_option(ty).is_some() {
601                        quote! {
602                            #binding
603                                .as_ref()
604                                .map(|__s| ::pseudo_backtrace::Chain::Std(__s.as_dyn_std_error()))
605                        }
606                    } else {
607                        quote! {
608                            ::core::option::Option::Some(::pseudo_backtrace::Chain::Std(
609                                #binding.as_dyn_std_error(),
610                            ))
611                        }
612                    }
613                } else if type_parameter_of_option(ty).is_some() {
614                    quote! {
615                        #binding
616                            .as_ref()
617                            .map(|__s| ::pseudo_backtrace::Chain::Stacked(__s.as_dyn_stack_error()))
618                    }
619                } else {
620                    quote! {
621                        ::core::option::Option::Some(::pseudo_backtrace::Chain::Stacked(
622                            #binding.as_dyn_stack_error(),
623                        ))
624                    }
625                }
626            }
627            None => quote! { ::core::option::Option::None },
628        }
629    }
630}
631
632// Detect Option<T> and return T if present
633fn type_parameter_of_option(ty: &syn::Type) -> Option<&syn::Type> {
634    let path = match ty {
635        syn::Type::Path(ty) => &ty.path,
636        _ => return None,
637    };
638    let last = path.segments.last()?;
639    if last.ident != "Option" {
640        return None;
641    }
642    let args = match &last.arguments {
643        syn::PathArguments::AngleBracketed(args) => args,
644        _ => return None,
645    };
646    if args.args.len() != 1 {
647        return None;
648    }
649    match &args.args[0] {
650        syn::GenericArgument::Type(inner) => Some(inner),
651        _ => None,
652    }
653}
654
655// Returns true if the type is `LocatedError<..>` (ignoring full path qualifiers)
656fn is_located_error(ty: &syn::Type) -> bool {
657    // Peel references like `&T` for robustness
658    let ty = match ty {
659        syn::Type::Reference(r) => &*r.elem,
660        _ => ty,
661    };
662
663    let syn::Type::Path(type_path) = ty else {
664        return false;
665    };
666    let Some(last) = type_path.path.segments.last() else {
667        return false;
668    };
669    last.ident == "LocatedError"
670}
671
672// When no explicit `#[location]` exists, allow using a `LocatedError<_>` field
673// that is also marked as a source field as the location provider.
674fn resolve_location_from_located_source(fields: &[FieldInfo], allow_name: bool) -> Option<usize> {
675    // candidates are fields that are explicitly sources or named `source`
676    let mut candidates: Vec<usize> = Vec::new();
677    for (idx, field) in fields.iter().enumerate() {
678        if field.attrs.is_source || field.attrs.is_terminal {
679            candidates.push(idx);
680            continue;
681        }
682        if allow_name
683            && let Some(ident) = &field.ident
684            && ident == "source"
685        {
686            candidates.push(idx);
687        }
688    }
689
690    // Choose the first candidate whose type is LocatedError<_>
691    let picked = candidates
692        .into_iter()
693        .find(|&idx| is_located_error(&fields[idx].ty));
694    if picked.is_some() {
695        return picked;
696    }
697    None
698}
699
700// Fallback: if there is exactly one field and it's LocatedError<_>, use it as location
701fn single_field_located(fields: &[FieldInfo]) -> Option<usize> {
702    if fields.len() == 1 && is_located_error(&fields[0].ty) {
703        Some(0)
704    } else {
705        None
706    }
707}
708
709struct BoundsTracker {
710    params: BTreeMap<String, Ident>,
711    needs_error: BTreeSet<String>,
712    needs_stack: BTreeSet<String>,
713}
714
715impl BoundsTracker {
716    fn new(generics: &Generics) -> Self {
717        let params = generics
718            .type_params()
719            .map(|param| (param.ident.to_string(), param.ident.clone()))
720            .collect();
721
722        BoundsTracker {
723            params,
724            needs_error: BTreeSet::new(),
725            needs_stack: BTreeSet::new(),
726        }
727    }
728
729    fn collect(&mut self, ty: &syn::Type, is_terminal: bool) {
730        let mut visitor = TypeParamCollector {
731            params: &self.params,
732            found: BTreeSet::new(),
733        };
734        visitor.visit_type(ty);
735
736        for name in visitor.found {
737            self.needs_error.insert(name.clone());
738            if !is_terminal {
739                self.needs_stack.insert(name);
740            }
741        }
742    }
743
744    fn apply(&self, generics: &mut Generics) {
745        for param in generics.type_params_mut() {
746            let name = param.ident.to_string();
747            if self.needs_stack.contains(&name) {
748                param
749                    .bounds
750                    .push(syn::parse_quote!(::pseudo_backtrace::StackError));
751            }
752            if self.needs_error.contains(&name) {
753                param.bounds.push(syn::parse_quote!(::core::error::Error));
754            }
755        }
756    }
757}
758
759struct TypeParamCollector<'a> {
760    params: &'a BTreeMap<String, Ident>,
761    found: BTreeSet<String>,
762}
763
764impl<'a, 'ast> Visit<'ast> for TypeParamCollector<'a> {
765    fn visit_type_path(&mut self, type_path: &'ast syn::TypePath) {
766        if type_path.qself.is_none()
767            && let Some(segment) = type_path.path.segments.first()
768        {
769            let ident = &segment.ident;
770            let name = ident.to_string();
771            if self.params.contains_key(&name) {
772                self.found.insert(name);
773            }
774        }
775
776        syn::visit::visit_type_path(self, type_path);
777    }
778}
779
780fn combine_error(acc: Option<syn::Error>, next: syn::Error) -> Option<syn::Error> {
781    match acc {
782        Some(mut err) => {
783            err.combine(next);
784            Some(err)
785        }
786        None => Some(next),
787    }
788}