dterror-derive 0.1.0

Derive macro for the dterror FromContext trait
Documentation
//! Generics validation for 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;

/// Check that the item's generics meet the necessary requirements.
///
/// `context_fields` is the list of context fields across every scope (the struct itself, or all
/// enum variants): the generated `{Error}Ctx` declares the item's full type-parameter list, so
/// every type parameter must be used by at least one context field somewhere.
pub(crate) fn validate_generics<'a>(
    input: &DeriveInput,
    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,
                    "`#[derive(FromContext)]` does not support lifetime parameters; the context must be `'static`",
                ));
            }
            GenericParam::Const(const_param) => {
                return Err(syn::Error::new_spanned(
                    const_param,
                    "`#[derive(FromContext)]` does not support const generic parameters",
                ));
            }
        }
    }

    // Every type parameter must appear in at least one context field type, otherwise the
    // generated `{Error}Ctx` struct would declare an unused type parameter. A type parameter
    // that appears only in a `#[context(borrow = ...)]` target is rejected with its own msg
    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();
    for cf in context_fields {
        let mut collector = UsedTypeParams {
            type_params: &type_params,
            used: &mut used_in_fields,
        };
        collector.visit_type(cf.ty());
        if let Some(target) = &cf.borrow_target {
            let mut collector = UsedTypeParams {
                type_params: &type_params,
                used: &mut used_in_borrow_targets,
            };
            collector.visit_type(target);
        }
    }

    for param in input.generics.type_params() {
        let ident = &param.ident;
        if !used_in_fields.contains(ident) && !used_in_borrow_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) {
            return Err(syn::Error::new_spanned(
                ident,
                "type parameter appears in a `#[context(borrow = ...)]` target, but no fields",
            ));
        }
    }

    Ok(())
}

/// Collects the type parameters referenced by a field type.
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 ty.qself.is_none()
            && ty.path.segments.len() == 1
            && let Some(segment) = ty.path.segments.first()
            && segment.arguments.is_none()
            && self.type_params.contains(&segment.ident)
        {
            self.used.insert(segment.ident.clone());
        }
        visit::visit_type_path(self, ty);
    }
}