dterror-derive 0.2.0

Proc-macros for the dterror CtxError and FromContext traits
Documentation
//! This module validates the generics of the derived error type and its generated `{Error}Ctx`.

use std::collections::HashSet;
use syn::{
    DeriveInput, GenericParam, Ident, TypePath,
    visit::{self, Visit},
};

use super::field::{CtxField, CtxTarget};

/// Validates that the generics of the item meet the requirements.
///
/// `context_fields` is the list of context fields across every scope (the struct itself, or all enum variants).
/// The generated Ctx type declares the full type-parameter list of the item.
/// Every type parameter must appear in at least one context field.
/// The derive rejects a parameter that appears only in a helper target (`borrow` or `from`).
/// The generated `From` implementation requires trait bounds that the derive does not generate.
pub(crate) fn validate_generics<'a>(
    input: &DeriveInput,
    derive_name: &str,
    context_fields: impl IntoIterator<Item = &'a CtxField<'a>>,
) -> syn::Result<()> {
    for param in &input.generics.params {
        match param {
            GenericParam::Type(_) => {}
            GenericParam::Lifetime(lifetime) => {
                return Err(syn::Error::new_spanned(
                    lifetime,
                    format!(
                        "`#[derive({derive_name})]` does not support lifetime parameters; the \
                         context must be `'static`"
                    ),
                ));
            }
            GenericParam::Const(const_param) => {
                return Err(syn::Error::new_spanned(
                    const_param,
                    format!("`#[derive({derive_name})]` does not support const generic parameters"),
                ));
            }
        }
    }

    let type_params: HashSet<Ident> = input
        .generics
        .type_params()
        .map(|param| param.ident.clone())
        .collect();
    if type_params.is_empty() {
        return Ok(());
    }

    let mut used_in_fields = HashSet::new();
    let mut used_in_borrow_targets = HashSet::new();
    let mut used_in_from_targets = HashSet::new();
    for cf in context_fields {
        collector(&type_params, &mut used_in_fields).visit_type(cf.ty());
        match &cf.target {
            CtxTarget::Borrow(target) => {
                collector(&type_params, &mut used_in_borrow_targets).visit_type(target);
            }
            CtxTarget::From(target) => {
                collector(&type_params, &mut used_in_from_targets).visit_type(target);
            }
            CtxTarget::Field => {}
        }
    }

    for param in input.generics.type_params() {
        let ident = &param.ident;
        if !used_in_fields.contains(ident)
            && !used_in_borrow_targets.contains(ident)
            && !used_in_from_targets.contains(ident)
        {
            return Err(syn::Error::new_spanned(
                ident,
                "type parameter does not appear in any context field",
            ));
        }
        if !used_in_fields.contains(ident) {
            // The derive prefers the `borrow` message when a parameter appears in targets of both kinds but not in any field.
            let message = if used_in_borrow_targets.contains(ident) {
                "type parameter appears in a `#[context(borrow = ...)]` target, but no fields"
            } else {
                "type parameter appears in a `#[context(from = ...)]` target, but no fields"
            };
            return Err(syn::Error::new_spanned(ident, message));
        }
    }

    Ok(())
}

fn collector<'s>(
    type_params: &'s HashSet<Ident>,
    used: &'s mut HashSet<Ident>,
) -> UsedTypeParams<'s> {
    UsedTypeParams { type_params, used }
}

/// A visitor that collects the type parameters that a field type uses.
struct UsedTypeParams<'a> {
    type_params: &'a HashSet<Ident>,
    used: &'a mut HashSet<Ident>,
}

impl<'ast> Visit<'ast> for UsedTypeParams<'_> {
    fn visit_type_path(&mut self, ty: &'ast TypePath) {
        if let Some(segment) = ty.path.segments.first()
            && ty.qself.is_none()
            && ty.path.segments.len() == 1
            && segment.arguments.is_none()
            && self.type_params.contains(&segment.ident)
        {
            self.used.insert(segment.ident.clone());
        }
        visit::visit_type_path(self, ty);
    }
}