dterror-derive 0.2.0

Proc-macros for the dterror CtxError and FromContext traits
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
//! This module expands the enum path. The macro generates the `{Error}Ctx` enum, one constructor
//! for each variant, and the `CtxError` impl.

use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::collections::HashMap;
use syn::{Attribute, DataEnum, DeriveInput, Fields, Ident, LitStr, Variant, parse_quote};

use super::derive_attr::{CtxDerives, parse_ctx_derives};
use super::field::{CtxTarget, ScopeFields, bind_lifetimes_to_ctx, contains_lifetime};
use super::naming::to_snek_case;
use super::validate::validate_generics;

/// This struct holds one constructible enum variant. It stores the original variant, its classified
/// fields, and the resolved constructor name. The default name is `snake_case`. A
/// `#[context(constructor = "...")]` attribute overrides that name.
struct CtxVariant<'a> {
    /// The field stores the original `syn` variant definition.
    variant: &'a Variant,
    /// The field stores the classified fields of this variant.
    scope: ScopeFields<'a>,
    /// The field stores the name of the generated constructor on `{Error}Ctx`.
    ctor_name: Ident,
}

/// The macro expands the enum path. It classifies every variant and validates names and generics.
/// Then it emits the Ctx enum, one constructor for each variant, and the `CtxError` impl.
pub(crate) fn expand_enum_item(
    input: &DeriveInput,
    data: &DataEnum,
    ctx_ident: &Ident,
) -> syn::Result<TokenStream2> {
    let derives = parse_ctx_derives(input)?;

    let mut ctx_variants: Vec<CtxVariant> = Vec::new();

    for variant in &data.variants {
        match &variant.fields {
            // The macro accepts unit variants but never treats them as constructible. Such a
            // variant gets no Ctx variant and no constructor.
            Fields::Unit => {}
            Fields::Unnamed(_) => {
                return Err(syn::Error::new_spanned(
                    variant,
                    "`#[derive(CtxError)]` only supports variants with named fields",
                ));
            }
            Fields::Named(fields) => {
                let scope = ScopeFields::classify(&fields.named)?;
                // A variant is constructible only if it has a source. Without a source, there is no
                // error that receives context. Context fields are optional. A variant without
                // context fields gets an empty unit Ctx variant and a constructor that takes no
                // arguments.
                if scope.source_field.is_none() {
                    continue;
                }
                let ctor_name = parse_constructor_override(variant)?
                    .unwrap_or_else(|| to_snek_case(&variant.ident));
                ctx_variants.push(CtxVariant {
                    variant,
                    scope,
                    ctor_name,
                });
            }
        }
    }

    if ctx_variants.is_empty() {
        return Err(syn::Error::new_spanned(
            input,
            "`#[derive(CtxError)]` requires at least one variant with a `#[source]`/`#[from]` field",
        ));
    }

    check_unique_constructor_names(ctx_variants.iter().map(|cv| (&cv.ctor_name, cv.variant)))?;

    // The Ctx enum declares the full type-parameter list of the item. The derive therefore validates
    // generics across all variants.
    let context_fields = ctx_variants
        .iter()
        .flat_map(|cv| cv.scope.context_fields.iter());
    validate_generics(input, "CtxError", context_fields)?;

    let vis = &input.vis;

    // The `'ctx` lifetime is global to the Ctx enum. A borrowed field in any variant requires it. A
    // from-target that mentions lifetimes also requires it, because the derive binds those lifetimes
    // to `'ctx`. When neither case applies, the derive omits `'ctx`. This omission avoids error
    // E0392, which reports an unused lifetime on an enum definition.
    let has_ctx_lifetime = ctx_variants
        .iter()
        .flat_map(|cv| cv.scope.context_fields.iter())
        .any(|cf| match &cf.target {
            CtxTarget::Borrow(_) => true,
            CtxTarget::From(target) => contains_lifetime(target),
            CtxTarget::Field => false,
        });

    let (impl_generics, error_type_generics, where_clause) = input.generics.split_for_impl();

    // The macro builds a second `Generics` set that inserts `'ctx` as the first lifetime parameter.
    let mut ctx_generics_lifetime = input.generics.clone();
    if has_ctx_lifetime {
        ctx_generics_lifetime.params.insert(0, parse_quote!('ctx));
    }
    let (ctx_impl_generics, ctx_type_generics, _) = ctx_generics_lifetime.split_for_impl();

    let ctx_enum = expand_enum(
        ctx_ident,
        &ctx_variants,
        vis,
        has_ctx_lifetime,
        &impl_generics,
        where_clause,
        &ctx_impl_generics,
        &derives,
    );

    let constructors = expand_enum_constructors(
        ctx_ident,
        &ctx_variants,
        has_ctx_lifetime,
        &impl_generics,
        &error_type_generics,
        where_clause,
        &ctx_impl_generics,
        &ctx_type_generics,
    );

    let ctx_error_impl = expand_enum_ctx_error(
        &input.ident,
        ctx_ident,
        &ctx_variants,
        has_ctx_lifetime,
        &impl_generics,
        &error_type_generics,
        where_clause,
        &ctx_type_generics,
    );

    Ok(quote! {
        #ctx_enum
        #constructors
        #ctx_error_impl
    })
}

/// The macro parses the optional variant-level `#[context(constructor = "name")]` attribute.
///
/// A variant carries at most one `#[context(...)]` attribute. Inside that attribute, the macro
/// accepts only the `constructor` helper. The helper takes a quoted string value. That value must
/// parse as an identifier.
pub(crate) fn parse_constructor_override(variant: &Variant) -> syn::Result<Option<Ident>> {
    let context_attrs: Vec<&Attribute> = variant
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident("context"))
        .collect();

    if let Some(duplicate) = context_attrs.get(1) {
        return Err(syn::Error::new_spanned(
            duplicate,
            "a variant may carry at most one `#[context(...)]` attribute",
        ));
    }

    let mut ctor_name: Option<Ident> = None;

    for attr in &context_attrs {
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("constructor") {
                if ctor_name.is_some() {
                    return Err(
                        meta.error("duplicate `#[context(constructor = ...)]` on the same variant")
                    );
                }
                let Ok(value) = meta.value() else {
                    return Err(meta.error(
                        "`#[context(constructor)]` requires a quoted string name, e.g. \
                             `#[context(constructor = \"io\")]`",
                    ));
                };
                let lit: LitStr = match value.parse() {
                    Ok(lit) => lit,
                    Err(_) => {
                        return Err(meta.error(
                            "`#[context(constructor)]` requires a quoted string name, e.g. \
                             `#[context(constructor = \"io\")]`",
                        ));
                    }
                };
                ctor_name = Some(match lit.parse::<Ident>() {
                    Ok(ident) => ident,
                    Err(_) => {
                        return Err(meta.error(
                            "constructor name must be a valid identifier, e.g. \
                             `#[context(constructor = \"io\")]`",
                        ));
                    }
                });
            } else {
                return Err(meta.error(
                    "unknown `#[context(...)]` helper on a variant; expected `constructor`",
                ));
            }
            Ok(())
        })?;
    }

    Ok(ctor_name)
}

/// The macro rejects two variants that generate constructors with the same name.
pub(crate) fn check_unique_constructor_names<'a>(
    items: impl IntoIterator<Item = (&'a Ident, &'a Variant)>,
) -> syn::Result<()> {
    let mut seen: HashMap<Ident, &Variant> = HashMap::new();
    for (ctor_name, variant) in items {
        if let Some(first) = seen.get(ctor_name) {
            return Err(syn::Error::new_spanned(
                &variant.ident,
                format!(
                    "duplicate constructor name `{}`; variants `{}` and `{}` both generate it",
                    ctor_name, first.ident, variant.ident
                ),
            ));
        }
        seen.insert(ctor_name.clone(), variant);
    }
    Ok(())
}

/// The macro generates the `{Error}Ctx<'ctx>` enum. It creates one same-named variant for each
/// constructible variant. Each variant holds the context fields of its source variant in the
/// declaration order. A borrowed field stores `&'ctx TargetType`. A from-marked field stores the owned type of
/// its target. The derive binds every lifetime that the target mentions to `'ctx`. A plain field
/// stores its own type. The `#[context(derive(...))]` attribute selects the traits that the enum
/// derives. By default, the enum derives nothing.
#[allow(clippy::too_many_arguments)]
fn expand_enum(
    ctx_ident: &Ident,
    ctx_variants: &[CtxVariant<'_>],
    vis: &syn::Visibility,
    has_ctx_lifetime: bool,
    impl_generics: &syn::ImplGenerics<'_>,
    where_clause: Option<&syn::WhereClause>,
    ctx_impl_generics: &syn::ImplGenerics<'_>,
    derives: &CtxDerives,
) -> TokenStream2 {
    let variants = ctx_variants.iter().map(|cv| {
        let vident = &cv.variant.ident;
        if cv.scope.context_fields.is_empty() {
            return quote! { #vident };
        }
        let ctx_fields = cv.scope.context_fields.iter().map(|cf| {
            let fvis = cf.vis();
            let ident = cf.ident();
            match &cf.target {
                CtxTarget::Borrow(target) => quote! { #fvis #ident: &'ctx #target },
                CtxTarget::From(target) => {
                    let target = bind_lifetimes_to_ctx(target);
                    quote! { #fvis #ident: #target }
                }
                CtxTarget::Field => {
                    let ty = cf.ty();
                    quote! { #fvis #ident: #ty }
                }
            }
        });
        quote! { #vident { #(#ctx_fields,)* } }
    });

    // When the Ctx needs a `'ctx` lifetime parameter, the derive uses the injected generics.
    let enum_generics = if has_ctx_lifetime {
        quote! { #ctx_impl_generics }
    } else {
        quote! { #impl_generics }
    };

    let derive_attr = derives.derive_attr();

    quote! {
        #[allow(dead_code)]
        #derive_attr
        #vis enum #ctx_ident #enum_generics #where_clause {
            #(#variants,)*
        }
    }
}

/// The macro generates one `pub fn` for each constructible variant on `{Error}Ctx`. It places all
/// functions in a single impl block. A borrowed field takes `&'ctx TargetType` directly so that the
/// macro does not clone or allocate. A from-marked field and a plain field take `impl Into<...>` of
/// the type stored on `{Error}Ctx`.
#[allow(clippy::too_many_arguments)]
fn expand_enum_constructors(
    ctx_ident: &Ident,
    ctx_variants: &[CtxVariant<'_>],
    has_ctx_lifetime: bool,
    impl_generics: &syn::ImplGenerics<'_>,
    error_type_generics: &syn::TypeGenerics<'_>,
    where_clause: Option<&syn::WhereClause>,
    ctx_impl_generics: &syn::ImplGenerics<'_>,
    ctx_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
    let fns = ctx_variants.iter().map(|cv| {
        let vident = &cv.variant.ident;
        let name = &cv.ctor_name;
        if cv.scope.context_fields.is_empty() {
            let doc = format!("Construct the `{vident}` context variant.");
            return quote! {
                #[doc = #doc]
                pub fn #name() -> Self {
                    Self::#vident
                }
            };
        }
        let args = cv.scope.context_fields.iter().map(|cf| {
            let ident = cf.ident();
            match &cf.target {
                CtxTarget::Borrow(target) => quote! { #ident: &'ctx #target },
                CtxTarget::From(target) => {
                    let target = bind_lifetimes_to_ctx(target);
                    quote! { #ident: impl Into<#target> }
                }
                CtxTarget::Field => {
                    let ty = cf.ty();
                    quote! { #ident: impl Into<#ty> }
                }
            }
        });
        let body = cv.scope.context_fields.iter().map(|cf| {
            let ident = cf.ident();
            if let CtxTarget::Borrow(_) = &cf.target {
                quote! { #ident: #ident, }
            } else {
                quote! { #ident: #ident.into(), }
            }
        });
        let doc = format!("Construct the `{vident}` context variant from one argument per context field, in declaration order.");
        quote! {
            #[doc = #doc]
            pub fn #name(#(#args),*) -> Self {
                Self::#vident {
                    #(#body)*
                }
            }
        }
    });

    let impl_generics_for_ctor = if has_ctx_lifetime {
        quote! { #ctx_impl_generics }
    } else {
        quote! { #impl_generics }
    };
    let type_generics_for_ctor = if has_ctx_lifetime {
        quote! { #ctx_type_generics }
    } else {
        quote! { #error_type_generics }
    };

    quote! {
        impl #impl_generics_for_ctor #ctx_ident #type_generics_for_ctor #where_clause {
            #(#fns)*
        }
    }
}

/// The macro generates `impl ::dterror::CtxError for {Error}`. It matches the Ctx value to build the
/// corresponding error variant. It converts borrow targets and from-marked surrogates back to owned
/// values with `.into()` when needed. It assigns the `location` field and the `source` field of each
/// variant. The macro wraps an optional source in `Some(...)`.
#[allow(clippy::too_many_arguments)]
fn expand_enum_ctx_error(
    error_ident: &Ident,
    ctx_ident: &Ident,
    ctx_variants: &[CtxVariant<'_>],
    has_ctx_lifetime: bool,
    impl_generics: &syn::ImplGenerics<'_>,
    error_type_generics: &syn::TypeGenerics<'_>,
    where_clause: Option<&syn::WhereClause>,
    ctx_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
    let arms = ctx_variants.iter().map(|cv| {
        let vident = &cv.variant.ident;
        let location_init = cv.scope.location_field.map(|field| {
            let ident = &field.ident;
            quote! { #ident: location, }
        });
        let source_init = cv.scope.source_field.map(|field| {
            let ident = &field.ident;
            if cv.scope.source_is_option {
                quote! { #ident: ::std::option::Option::Some(source), }
            } else {
                quote! { #ident: source, }
            }
        });
        if cv.scope.context_fields.is_empty() {
            // The Ctx variant is empty, that is a unit variant. The arm matches it by name. The arm
            // fills only the location field and the source field.
            return quote! {
                #ctx_ident::#vident => #error_ident::#vident {
                    #location_init
                    #source_init
                },
            };
        }
        let bindings = cv
            .scope
            .context_fields
            .iter()
            .map(crate::field::CtxField::ident);
        let inits = cv.scope.context_fields.iter().map(|cf| {
            let ident = cf.ident();
            if let CtxTarget::Field = &cf.target {
                quote! { #ident: #ident, }
            } else {
                quote! { #ident: #ident.into(), }
            }
        });
        quote! {
            #ctx_ident::#vident { #(#bindings),* } => #error_ident::#vident {
                #(#inits)*
                #location_init
                #source_init
            },
        }
    });

    // The GAT type alias references `ctx_ident`. It includes `'ctx` when a field is borrowed, and it
    // omits `'ctx` otherwise. When no field is borrowed, `type Ctx<'ctx> = {Error}Ctx<T>;` is still
    // valid.
    let gat_type_alias = if has_ctx_lifetime {
        quote! { #ctx_ident #ctx_type_generics }
    } else {
        quote! { #ctx_ident #error_type_generics }
    };

    quote! {
        #[automatically_derived]
        impl #impl_generics ::dterror::CtxError for #error_ident #error_type_generics #where_clause {
            type Ctx<'ctx> = #gat_type_alias;

            fn from_context(
                ctx: Self::Ctx<'_>,
                location: &'static std::panic::Location<'static>,
                source: Box<dyn std::error::Error + Send + Sync + 'static>,
            ) -> Self {
                match ctx {
                    #(#arms)*
                }
            }
        }
    }
}