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 resolve_location(&fields, allow_name, ident.span())?
54 } else {
55 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
632fn 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
655fn is_located_error(ty: &syn::Type) -> bool {
657 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
672fn resolve_location_from_located_source(fields: &[FieldInfo], allow_name: bool) -> Option<usize> {
675 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 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
700fn 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}