nu-test-support-macros 0.113.0

Macros for nu-test-support
Documentation
use proc_macro2::*;
use quote::{ToTokens, TokenStreamExt, quote, quote_spanned};

pub fn test_cell_path(tokens: TokenStream) -> Result<TokenStream, Error> {
    let path_members = parse_tokens(tokens)?;
    Ok(quote! {
        ::nu_protocol::ast::CellPath {
            members: ::std::vec![#(#path_members),*]
        }
    })
}

pub struct Error {
    span: Span,
    kind: ErrorKind,
}
pub enum ErrorKind {
    EmptyTokenStream,
    ExpectedValue,
    ExpectedModifierOrDot,
    DotWithoutFollowingComponent,
    DuplicateOptional,
    DuplicateInsensitive,
    UnexpectedPunct,
    MissingFinalValue,
}

impl Error {
    pub fn into_compile_error(self) -> TokenStream {
        let span = self.span;
        let msg = match self.kind {
            ErrorKind::EmptyTokenStream => "empty token stream is not allowed",
            ErrorKind::ExpectedValue => "expected group, ident or literal",
            ErrorKind::ExpectedModifierOrDot => "expected ! ? or .",
            ErrorKind::DotWithoutFollowingComponent => "dot without following component",
            ErrorKind::DuplicateOptional => "duplicate ?",
            ErrorKind::DuplicateInsensitive => "duplicate !",
            ErrorKind::UnexpectedPunct => "unexpected punctuation",
            ErrorKind::MissingFinalValue => "missing final path member",
        };

        quote_spanned! { span =>
            compile_error!(#msg)
        }
    }
}

struct PathMember {
    value: PathMemberValue,
    optional: Option<Punct>,
    insensitive: Option<Punct>,
}

impl ToTokens for PathMember {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let PathMember {
            value,
            optional,
            insensitive,
        } = self;

        let ts = match (optional, insensitive) {
            (None, None) => quote! {{
                ::nu_protocol::ast::TestPathMember::from(#value).into_path_member()
            }},
            (Some(_), None) => quote! {{
                let mut path_member = ::nu_protocol::ast::TestPathMember::from(#value).into_path_member();
                path_member.make_optional();
                path_member
            }},
            (None, Some(_)) => quote! {{
                let mut path_member = ::nu_protocol::ast::TestPathMember::from(#value).into_path_member();
                path_member.make_insensitive();
                path_member
            }},
            (Some(_), Some(_)) => quote! {{
                let mut path_member = ::nu_protocol::ast::TestPathMember::from(#value).into_path_member();
                path_member.make_optional();
                path_member.make_insensitive();
                path_member
            }},
        };

        tokens.append_all(ts)
    }
}

enum PathMemberValue {
    Group(Group),
    Ident(Ident),
    Literal(Literal),
}

impl ToTokens for PathMemberValue {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            PathMemberValue::Group(group) => group.to_tokens(tokens),
            PathMemberValue::Ident(ident) => {
                let mut literal = Literal::string(&ident.to_string());
                literal.set_span(ident.span());
                literal.to_tokens(tokens);
            }
            PathMemberValue::Literal(literal) => literal.to_tokens(tokens),
        }
    }
}

fn parse_tokens(tokens: TokenStream) -> Result<Vec<PathMember>, Error> {
    use PathMemberValue as PMV;
    use TokenTree as TT;

    if tokens.is_empty() {
        return Err(Error {
            span: Span::call_site(),
            kind: ErrorKind::EmptyTokenStream,
        });
    }

    let mut tokens = tokens.into_iter().peekable();
    let mut values = Vec::new();
    let mut value = None::<PathMemberValue>;
    let mut optional = None::<Punct>;
    let mut insensitive = None::<Punct>;

    while let Some(tt) = tokens.next() {
        let span = tt.span();
        let err = |kind| Error { span, kind };
        match (tt, &value, &optional, &insensitive) {
            (TT::Group(v), None, None, None) => value = Some(PMV::Group(v)),
            (TT::Ident(v), None, None, None) => value = Some(PMV::Ident(v)),
            (TT::Literal(v), None, None, None) => value = Some(PMV::Literal(v)),

            (TT::Punct(_), None, _, _) => return Err(err(ErrorKind::ExpectedValue)),

            (TT::Group(_) | TT::Ident(_) | TT::Literal(_), Some(_), _, _) => {
                return Err(err(ErrorKind::ExpectedModifierOrDot));
            }

            (TT::Punct(punct), Some(_), _, _) => match punct.as_char() {
                '.' => match tokens.peek() {
                    None => return Err(err(ErrorKind::DotWithoutFollowingComponent)),
                    Some(_) => values.push(PathMember {
                        value: value.take().expect("is some"),
                        optional: optional.take(),
                        insensitive: insensitive.take(),
                    }),
                },

                '?' => match optional {
                    None => optional = Some(punct),
                    Some(_) => return Err(err(ErrorKind::DuplicateOptional)),
                },

                '!' => match insensitive {
                    None => insensitive = Some(punct),
                    Some(_) => return Err(err(ErrorKind::DuplicateInsensitive)),
                },

                _ => return Err(err(ErrorKind::UnexpectedPunct)),
            },

            (TT::Group(_), None, _, _)
            | (TT::Ident(_), None, _, _)
            | (TT::Literal(_), None, _, _) => {
                return Err(err(ErrorKind::ExpectedValue));
            }
        }
    }

    let Some(value) = value.take() else {
        return Err(Error {
            span: Span::call_site(),
            kind: ErrorKind::MissingFinalValue,
        });
    };

    values.push(PathMember {
        value,
        optional,
        insensitive,
    });

    Ok(values)
}

#[cfg(test)]
mod tests {
    use super::*;
    use proc_macro2::TokenStream;

    #[test]
    fn error_empty_token_stream() {
        let Err(err) = parse_tokens(TokenStream::new()) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::EmptyTokenStream));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("empty token stream is not allowed"));
    }

    #[test]
    fn error_expected_value() {
        let Err(err) = parse_tokens(quote!(.)) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::ExpectedValue));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("expected group, ident or literal"));
    }

    #[test]
    fn error_expected_modifier_or_dot() {
        let Err(err) = parse_tokens(quote!(foo bar)) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::ExpectedModifierOrDot));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("expected ! ? or ."));
    }

    #[test]
    fn error_dot_without_following_component() {
        let Err(err) = parse_tokens(quote!(foo.)) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::DotWithoutFollowingComponent));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("dot without following component"));
    }

    #[test]
    fn error_duplicate_optional() {
        let Err(err) = parse_tokens(quote!(foo??)) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::DuplicateOptional));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("duplicate ?"));
    }

    #[test]
    fn error_duplicate_insensitive() {
        let Err(err) = parse_tokens(quote!(foo!!)) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::DuplicateInsensitive));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("duplicate !"));
    }

    #[test]
    fn error_unexpected_punct() {
        let Err(err) = parse_tokens(quote!(foo,)) else {
            panic!("expected error");
        };

        assert!(matches!(&err.kind, ErrorKind::UnexpectedPunct));
        let msg = err.into_compile_error().to_string();
        assert!(msg.contains("unexpected punctuation"));
    }
}