plux-codegen 0.3.0

A code generator for plux-rs
Documentation
use proc_macro2::TokenStream;
use syn::{
    Error, FnArg, GenericArgument, ItemFn, Pat, PathArguments, Result, Signature, Type, TypePath,
};

use super::utils::pat_to_ident;

pub(crate) fn validate(ast: &ItemFn, _: &TokenStream) -> Result<()> {
    if !ast.sig.generics.params.is_empty() {
        return Err(Error::new_spanned(ast, "generics are not supported"));
    }

    validate_function(&ast.sig)
}

fn validate_function(sig: &Signature) -> Result<()> {
    validate_externals(&sig.inputs[0])?;
    validate_args(sig.inputs.iter().skip(1))?;

    if let syn::ReturnType::Type(_, ref ty) = sig.output {
        validate_output_type(ty.as_ref())?;
    }

    Ok(())
}

fn validate_externals(exts: &FnArg) -> Result<()> {
    match exts {
        FnArg::Receiver(_) => Err(Error::new_spanned(exts, "Receiver is not supported")),
        FnArg::Typed(pat) => match &*pat.ty {
            Type::Tuple(_) => match &*pat.pat {
                Pat::Tuple(tuple) => tuple
                    .elems
                    .iter()
                    .try_for_each(|pat| validate_externals_name(pat)),
                pat => validate_externals_name(pat),
            },
            _ => validate_externals_name(&*pat.pat),
        },
    }
}

fn validate_externals_name(pat: &Pat) -> Result<()> {
    match pat_to_ident(pat) {
        Ok(_) => Ok(()),
        Err(_) => Err(Error::new_spanned(
            pat,
            "name of the external is specified incorrectly",
        )),
    }
}

fn validate_args<'a, I>(mut args: I) -> Result<()>
where
    I: Iterator<Item = &'a FnArg>,
{
    args.try_for_each(|arg| match arg {
        FnArg::Receiver(_) => Err(Error::new_spanned(arg, "Receiver is not supported")),
        FnArg::Typed(pat) => validate_type(&*pat.ty, true),
    })?;

    Ok(())
}

fn validate_output_type(ty: &Type) -> Result<()> {
    match ty {
        Type::Infer(_) | Type::Never(_) => Ok(()),
        Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()),
        Type::Path(path) => match path.path.segments.last() {
            Some(segment) if segment.ident == "Result" => match &segment.arguments {
                PathArguments::AngleBracketed(args) => {
                    if let Some(GenericArgument::Type(ty)) = args.args.first() {
                        match ty {
                            Type::Tuple(tuple) => tuple.elems.is_empty().then(|| Ok(())).unwrap_or_else(|| {
                                Err(Error::new_spanned(
                                    tuple,
                                    "type must contain only (), _, !, T, Result<T, ...> or Result<(), ...>",
                                ))
                            }),
                            _ => validate_type(ty, false),
                        }
                    } else {
                        Err(Error::new_spanned(
                            args.args.first().unwrap(),
                            "Result must contain only a type",
                        ))
                    }
                }
                _ => Err(Error::new_spanned(
                    segment,
                    "type must contain only (), _, !, T, Result<T, ...> or Result<(), ...>",
                )),
            },
            _ => validate_type_path(path, false),
        },
        _ => Err(Error::new_spanned(
            ty,
            "type must contain only (), _, !, T or Result<T, ...> or Result<(), ...>",
        )),
    }
}

fn validate_type(ty: &Type, is_ref: bool) -> Result<()> {
    match is_ref {
        true => match ty {
            Type::Path(path) => validate_type_path(&path, is_ref),
            Type::Reference(r) => match r.mutability {
                None => validate_type(&*r.elem, false),
                _ => Err(Error::new_spanned(
                    ty,
                    "type must not contain a mutated reference",
                )),
            },
            ty => Err(Error::new_spanned(
                ty,
                "type must contain only references (&T) or Vec<&T>",
            )),
        },
        false => match ty {
            Type::Path(path) => validate_type_path(&path, is_ref),
            ty => Err(Error::new_spanned(
                ty,
                "type must contain only literals (T)",
            )),
        },
    }
}

const VALIDATE_TYPE: [&str; 15] = [
    "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "bool", "char", "String",
    "Vec", "Variable",
];

fn validate_type_path(path: &TypePath, is_ref: bool) -> Result<()> {
    let segment = path.path.segments.last().unwrap();
    let ty = segment.ident.to_string();

    if VALIDATE_TYPE.contains(&ty.as_str()) {
        if ty == "Vec" {
            match &segment.arguments {
                PathArguments::AngleBracketed(args) => {
                    let arg = args.args.first().unwrap();
                    match arg {
                        GenericArgument::Type(ty) => return validate_type(ty, is_ref),
                        _ => return Err(Error::new_spanned(arg, "Vec must contain only a type")),
                    }
                }
                _ => (),
            }
        } else if is_ref {
            return Err(Error::new_spanned(
                path,
                "type must contain only references (&T) or Vec<&T>",
            ));
        }
    } else {
        return Err(Error::new_spanned(path, "type is not supported"));
    }

    Ok(())
}