moxy-token 0.4.3

Rust token types and lexing
Documentation
use super::ToTokens;
use super::lex::{Cursor, LexError, Scan};
use crate::{Spacing, Span, Spanner, TokenStream, TokenTree};

fn spacing_after(text: &str, cursor: Cursor<'_>) -> Spacing {
    let Some(next) = cursor.first() else {
        return Spacing::Alone;
    };

    let current = text.chars().next().unwrap_or_default();
    let is_compound_pair = matches!(
        (current, next),
        ('&', '&' | '=')
            | ('|', '|' | '=')
            | ('<', '<' | '=' | '-')
            | ('>', '>' | '=')
            | ('=', '=' | '>')
            | ('!', '=')
            | ('+', '=')
            | ('-', '=' | '>')
            | ('*', '=')
            | ('/', '=')
            | ('%', '=')
            | ('^', '=')
            | (':', ':')
            | ('.', '.' | '=')
    );

    if is_compound_pair || (current == '\'' && (next == '_' || next.is_alphabetic())) {
        Spacing::Joint
    } else {
        Spacing::Alone
    }
}

macro_rules! define_punct {
    ($($name:ident[$is_method:ident, $as_method:ident] => $text:literal),+ $(,)?) => {
        /// A Rust punctuation token.
        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
        pub enum Punct {
            $($name($name),)*
        }

        impl Punct {
            pub fn as_str(&self) -> &'static str {
                match self {
                    $(Self::$name(v) => v.as_str(),)*
                }
            }

            pub fn span(&self) -> Span {
                match self {
                    $(Self::$name(v) => v.span(),)*
                }
            }

            pub fn set_span(&mut self, span: Span) {
                match self {
                    $(Self::$name(v) => v.set_span(span),)*
                }
            }

            pub fn spacing(&self) -> Spacing {
                match self {
                    $(Self::$name(v) => v.spacing(),)*
                }
            }

            pub fn set_spacing(&mut self, spacing: Spacing) {
                match self {
                    $(Self::$name(v) => v.set_spacing(spacing),)*
                }
            }

            #[inline]
            pub fn to_token_tree(&self) -> TokenTree {
                TokenTree::Punct(*self)
            }

            #[inline]
            pub fn into_token_tree(self) -> TokenTree {
                TokenTree::Punct(self)
            }
        }

        impl ToTokens for Punct {
            fn to_tokens(&self, tokens: &mut TokenStream) {
                match self {
                    $(Self::$name(v) => v.to_tokens(tokens),)*
                }
            }
        }

        impl Spanner for Punct {
            fn span(&self) -> Span {
                self.span()
            }
        }

        impl std::fmt::Display for Punct {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    $(Self::$name(v) => v.fmt(f),)*
                }
            }
        }

        impl Scan for Punct {
            fn scan(cursor: Cursor<'_>) -> Result<(Cursor<'_>, Self), LexError> {
                let mut end = cursor;
                let mut best = None;

                while let Some(ch) = end.first() && ch.is_ascii_punctuation() {
                    end = end.advance_by(ch.len_utf8());
                    let text = cursor.slice_to(end);

                    $(
                        if $name::TEXT == text {
                            best = Some((end, Self::$name($name {
                                span: cursor.span_to(&end),
                                spacing: spacing_after($name::TEXT, end),
                            })));
                            continue;
                        }
                    )*

                    break;
                }

                best.ok_or_else(|| cursor.error())
            }
        }

        #[cfg(feature = "serde")]
        impl serde::Serialize for Punct {
            fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
            where
                S: serde::Serializer,
            {
                self.as_str().serialize(s)
            }
        }

        $(
            #[doc = concat!("The Rust punctuation token `", $text, "`. ")]
            #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
            pub struct $name {
                span: Span,
                spacing: Spacing,
            }

            impl $name {
                pub const TEXT: &'static str = $text;

                pub fn new(span: Span) -> Self {
                    Self {
                        span,
                        spacing: Spacing::Alone,
                    }
                }

                pub fn span(&self) -> Span {
                    self.span
                }

                pub fn set_span(&mut self, span: Span) {
                    self.span = span;
                }

                pub fn spacing(&self) -> Spacing {
                    self.spacing
                }

                pub fn set_spacing(&mut self, spacing: Spacing) {
                    self.spacing = spacing;
                }

                pub fn with_spacing(mut self, spacing: Spacing) -> Self {
                    self.spacing = spacing;
                    self
                }

                pub fn as_str(&self) -> &'static str {
                    Self::TEXT
                }

                #[inline]
                pub fn to_token_tree(&self) -> TokenTree {
                    TokenTree::Punct((*self).into())
                }

                #[inline]
                pub fn into_token_tree(self) -> TokenTree {
                    TokenTree::Punct(self.into())
                }
            }

            impl std::fmt::Display for $name {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    f.write_str($text)
                }
            }

            impl Scan for $name {
                fn scan(cursor: Cursor<'_>) -> Result<(Cursor<'_>, Self), LexError> {
                    let end = cursor.advance_by($text.len());
                    let text = cursor.slice_to(end);

                    if text == $text {
                        Ok((
                            end,
                            Self::new(cursor.span_to(&end)).with_spacing(spacing_after(Self::TEXT, end)),
                        ))
                    } else {
                        cursor.error().into()
                    }
                }
            }

            impl ToTokens for $name {
                fn to_tokens(&self, tokens: &mut TokenStream) {
                    tokens.extend_one(TokenTree::Punct(Punct::$name(*self)));
                }
            }

            impl Spanner for $name {
                fn span(&self) -> Span {
                    self.span
                }
            }

            impl From<$name> for Punct {
                fn from(value: $name) -> Self {
                    Self::$name(value)
                }
            }

            #[cfg(feature = "serde")]
            impl serde::Serialize for $name {
                fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
                where
                    S: serde::Serializer,
                {
                    self.as_str().serialize(s)
                }
            }
        )+

        impl TokenTree {
            pub fn is_punct(&self) -> bool {
                matches!(self, Self::Punct(_))
            }

            pub fn as_punct(&self) -> Option<&Punct> {
                match self {
                    Self::Punct(v) => Some(v),
                    _ => None,
                }
            }

            $(
                #[doc = concat!("**", stringify!($name), "** (\"", $text, "\")")]
                pub fn $is_method(&self) -> bool {
                    matches!(self, Self::Punct(Punct::$name(_)))
                }

                #[doc = concat!("**", stringify!($name), "** (\"", $text, "\")")]
                pub fn $as_method(&self) -> Option<&$name> {
                    match self {
                        Self::Punct(Punct::$name(v)) => Some(v),
                        _ => None,
                    }
                }
            )*
        }
    };

}

define_punct! {
    And[is_punct_and, as_punct_and]                         => "&",
    Or[is_punct_or, as_punct_or]                            => "|",
    Not[is_punct_not, as_punct_not]                         => "!",
    Tilde[is_punct_tilde, as_punct_tilde]                   => "~",
    Plus[is_punct_plus, as_punct_plus]                      => "+",
    Minus[is_punct_minus, as_punct_minus]                   => "-",
    Underscore[is_punct_underscore, as_punct_underscore]    => "_",
    Star[is_punct_star, as_punct_star]                      => "*",
    Slash[is_punct_slash, as_punct_slash]                   => "/",
    Percent[is_punct_percent, as_punct_percent]             => "%",
    Caret[is_punct_caret, as_punct_caret]                   => "^",
    Eq[is_punct_eq, as_punct_eq]                            => "=",
    Lt[is_punct_lt, as_punct_lt]                            => "<",
    Gt[is_punct_gt, as_punct_gt]                            => ">",
    At[is_punct_at, as_punct_at]                            => "@",
    Dot[is_punct_dot, as_punct_dot]                         => ".",
    Comma[is_punct_comma, as_punct_comma]                   => ",",
    Semi[is_punct_semi, as_punct_semi]                      => ";",
    Colon[is_punct_colon, as_punct_colon]                   => ":",
    Pound[is_punct_pound, as_punct_pound]                   => "#",
    Dollar[is_punct_dollar, as_punct_dollar]                => "$",
    Question[is_punct_question, as_punct_question]          => "?",
    Quote[is_punct_quote, as_punct_quote]                   => "'",
}