checkito_macro 0.5.8

A set of macros to accompany the `checkito` crate.
Documentation
use quote::quote_spanned;
use syn::{
    __private::{Span, TokenStream2},
    Block, Expr, ExprBinary, ExprBlock, ExprCast, ExprConst, ExprGroup, ExprLit, ExprRange,
    ExprTuple, ExprUnary, Ident, Lit, RangeLimits, Stmt, Type, TypeGroup, TypeParen, TypePath,
    spanned::Spanned,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
    None,
    Default,
    Character(char),
}

#[derive(Debug, Clone)]
struct Pack {
    module: &'static str,
    constant: &'static str,
    kind: Kind,
    span: Span,
}

impl Kind {
    pub fn merge(left: Self, right: Self) -> Self {
        match (left, right) {
            (Kind::None, Kind::None) => Kind::None,
            (Kind::Default, right) => right,
            (left, Kind::Default) => left,
            (_, right @ Kind::Character(_)) => right,
            (Kind::Character(value), Kind::None) => Kind::Character(value),
        }
    }
}

impl Pack {
    pub fn new(module: &'static str, constant: &'static str, span: Span) -> Self {
        Self {
            module,
            constant,
            kind: Kind::None,
            span,
        }
    }

    pub fn module(&self) -> Ident {
        Ident::new(self.module, self.span)
    }

    pub fn constant(&self) -> Ident {
        Ident::new(self.constant, self.span)
    }

    pub fn default(span: Span) -> Self {
        Self {
            module: "i32",
            constant: "I32",
            kind: Kind::Default,
            span,
        }
    }

    pub fn character(value: char, span: Span) -> Self {
        Self {
            module: "char",
            constant: "Char",
            kind: Kind::Character(value),
            span,
        }
    }

    pub fn is_default(&self) -> bool {
        matches!(self.kind, Kind::Default)
    }

    pub fn limit(&self, expression: &Expr, limits: &RangeLimits) -> Option<TokenStream2> {
        match limits {
            RangeLimits::HalfOpen(_) => match self.kind {
                Kind::Character(value) => {
                    let value = char::from_u32(u32::checked_sub(value as u32, 1)?)?;
                    Some(quote_spanned!(expression.span() => #value))
                }
                _ => Some(quote_spanned!(expression.span() => #expression - 1)),
            },
            RangeLimits::Closed(_) => Some(quote_spanned!(expression.span() => #expression)),
        }
    }

    pub fn merge(left: Option<Self>, right: Option<Self>) -> Option<Self> {
        match (left, right) {
            (None, None) => None,
            (Some(left), None) => Some(left),
            (None, Some(right)) => Some(right),
            (Some(left), Some(right)) => {
                if left.is_default() {
                    Some(right)
                } else if right.is_default() {
                    Some(left)
                } else if left.module == right.module && left.constant == right.constant {
                    Some(Pack {
                        module: left.module,
                        constant: left.constant,
                        kind: Kind::merge(left.kind, right.kind),
                        span: left.span.join(right.span).unwrap_or(left.span),
                    })
                } else {
                    None
                }
            }
        }
    }
}

pub fn convert(expression: &Expr) -> Option<TokenStream2> {
    if let Some(pack) = unpack_expression(expression) {
        let module = pack.module();
        let constant = pack.constant();
        return Some(quote_spanned!(expression.span() => {
            #[allow(unused_braces)]
            #[allow(clippy::unnecessary_cast)]
            <::checkito::primitive::#module::#constant::<{ #expression }> as ::checkito::primitive::Constant>::VALUE
        }));
    }

    match expression {
        Expr::Group(ExprGroup { expr, .. }) => convert(expr),
        Expr::Const(ExprConst { block, .. }) if block.stmts.len() == 1 => {
            match block.stmts.last()? {
                Stmt::Expr(expr, None) => convert(expr),
                _ => None,
            }
        }
        Expr::Block(ExprBlock { block, .. }) if block.stmts.len() == 1 => {
            match block.stmts.last()? {
                Stmt::Expr(expr, None) => convert(expr),
                _ => None,
            }
        }
        Expr::Tuple(ExprTuple { elems, .. }) => {
            let mut items = Vec::new();
            for elem in elems {
                items.push(convert(elem)?);
            }
            Some(quote_spanned!(expression.span() => (#(#items,)*)))
        }
        Expr::Range(ExprRange {
            start, limits, end, ..
        }) => {
            let (start, end, pack) = match (start, end) {
                (None, None) => return None,
                (None, Some(end)) => {
                    let pack = unpack_expression(end)?;
                    let module = pack.module();
                    (
                        quote_spanned!(expression.span() => #module::MIN),
                        pack.limit(end, limits)?,
                        pack,
                    )
                }
                (Some(start), None) => {
                    let pack = unpack_expression(start)?;
                    let module = pack.module();
                    (
                        quote_spanned!(start.span() => #start),
                        quote_spanned!(expression.span() => #module::MAX),
                        pack,
                    )
                }
                (Some(start), Some(end)) => {
                    let pack = Pack::merge(unpack_expression(start), unpack_expression(end))?;
                    (
                        quote_spanned!(start.span() => #start),
                        pack.limit(end, limits)?,
                        pack,
                    )
                }
            };
            let module = pack.module();
            let constant = pack.constant();
            Some(quote_spanned!(expression.span() => {
                #[allow(unused_braces)]
                #[allow(clippy::unnecessary_cast)]
                <::checkito::primitive::Range::<::checkito::primitive::#module::#constant::<{ #start }>, ::checkito::primitive::#module::#constant::<{ #end }>> as ::checkito::primitive::Constant>::VALUE
            }))
        }
        _ => None,
    }
}

fn unpack_expression(expression: &Expr) -> Option<Pack> {
    match expression {
        Expr::Group(ExprGroup { expr, .. }) => unpack_expression(expr),
        Expr::Const(ExprConst { block, .. }) => unpack_block(block),
        Expr::Block(ExprBlock { block, .. }) => unpack_block(block),
        Expr::Cast(ExprCast { expr, ty, .. }) => {
            let pack = unpack_expression(expr)?;
            let value = match pack.kind {
                Kind::Character(value) => Some(value),
                _ => None,
            };
            unpack_type(ty, value)
        }
        Expr::Lit(ExprLit { lit, .. }) => unpack_literal(lit),
        Expr::Unary(ExprUnary { expr, .. }) => unpack_expression(expr),
        Expr::Binary(ExprBinary { left, right, .. }) => {
            Pack::merge(unpack_expression(left), unpack_expression(right))
        }
        _ => None,
    }
}

fn unpack_block(block: &Block) -> Option<Pack> {
    match block.stmts.last()? {
        Stmt::Expr(expr, None) => unpack_expression(expr),
        _ => None,
    }
}

fn unpack_literal(literal: &Lit) -> Option<Pack> {
    let span = literal.span();
    match literal {
        Lit::Bool(_) => Some(Pack::new("bool", "Bool", span)),
        Lit::Char(value) => Some(Pack::character(value.value(), span)),
        Lit::Byte(_) => Some(Pack::new("u8", "U8", span)),
        Lit::Int(value) if value.suffix().is_empty() => Some(Pack::default(span)),
        Lit::Int(value) => unpack_name(value.suffix(), None, span),
        _ => None,
    }
}

fn unpack_type(type_: &Type, value: Option<char>) -> Option<Pack> {
    match type_ {
        Type::Group(TypeGroup { elem, .. }) => unpack_type(elem, value),
        Type::Paren(TypeParen { elem, .. }) => unpack_type(elem, value),
        Type::Path(TypePath { qself: None, path }) => {
            unpack_name(path.get_ident()?.to_string().as_str(), value, path.span())
        }
        _ => None,
    }
}

fn unpack_name(name: &str, value: Option<char>, span: Span) -> Option<Pack> {
    match name {
        "bool" => Some(Pack::new("bool", "Bool", span)),
        "char" => Some(Pack::character(value?, span)),
        "u8" => Some(Pack::new("u8", "U8", span)),
        "u16" => Some(Pack::new("u16", "U16", span)),
        "u32" => Some(Pack::new("u32", "U32", span)),
        "u64" => Some(Pack::new("u64", "U64", span)),
        "u128" => Some(Pack::new("u128", "U128", span)),
        "usize" => Some(Pack::new("usize", "Usize", span)),
        "i8" => Some(Pack::new("i8", "I8", span)),
        "i16" => Some(Pack::new("i16", "I16", span)),
        "i32" => Some(Pack::new("i32", "I32", span)),
        "i64" => Some(Pack::new("i64", "I64", span)),
        "i128" => Some(Pack::new("i128", "I128", span)),
        "isize" => Some(Pack::new("isize", "Isize", span)),
        _ => None,
    }
}