Skip to main content

cel_parser/
lex_lexer.rs

1//! Flattens Rust token trees into CEL tokens while preserving source spans.
2//!
3//! Construct a [`LexLexer`] from a `proc_macro2::TokenStream` iterator and consume
4//! it as an iterator of [`Token`] values. Groups emit matching opening and closing
5//! delimiters, booleans become literals, and adjacent punctuation combines into
6//! the supported operators. Documentation attributes become comment tokens.
7//!
8//! ```
9//! use cel_parser::lex_lexer::{LexLexer, Token};
10//!
11//! let input: proc_macro2::TokenStream = "value && true".parse()?;
12//! let mut lexer = LexLexer::new(input.into_iter());
13//! assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "value"));
14//! assert!(matches!(lexer.next(), Some(Token::Punct { op, .. }) if op == "&&"));
15//! assert!(matches!(lexer.next(), Some(Token::Literal(_))));
16//! assert!(lexer.next().is_none());
17//! # Ok::<(), proc_macro2::LexError>(())
18//! ```
19//!
20//! # Error Handling
21//!
22//! A `#` commits to a documentation attribute: `#[doc = "..."]` or `#![doc = "..."]`.
23//! Other attribute shapes and literals Syn cannot parse produce [`Token::Error`].
24//! Errors consume the inspected token trees; iteration may continue with the
25//! remaining input, but does not roll back a malformed attribute.
26
27use proc_macro2::{Delimiter, Ident, Spacing, Span, TokenTree};
28use syn::Lit;
29
30/// Provides a token's source span for diagnostics.
31///
32/// # Examples
33///
34/// ```
35/// use cel_parser::lex_lexer::HasSpan;
36///
37/// let input: proc_macro2::TokenStream = "name".parse()?;
38/// let token = input.into_iter().next().unwrap();
39/// assert_eq!(HasSpan::span(&token).source_text().as_deref(), Some("name"));
40/// # Ok::<(), proc_macro2::LexError>(())
41/// ```
42pub trait HasSpan {
43    /// Returns the source span associated with this token.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use cel_parser::lex_lexer::HasSpan;
49    ///
50    /// let input: proc_macro2::TokenStream = "42".parse()?;
51    /// let token = input.into_iter().next().unwrap();
52    /// assert_eq!(HasSpan::span(&token).source_text().as_deref(), Some("42"));
53    /// # Ok::<(), proc_macro2::LexError>(())
54    /// ```
55    fn span(&self) -> Span;
56}
57
58/// Owns the unconsumed contents and matching close delimiter of one open group.
59struct GroupLevel {
60    iter: TokenStreamIter,
61    delimiter: Delimiter,
62    span: Span,
63}
64
65/// Owns a token stream's consuming iterator.
66///
67/// # Examples
68///
69/// ```
70/// use cel_parser::lex_lexer::TokenStreamIter;
71///
72/// let input: proc_macro2::TokenStream = "42".parse()?;
73/// let mut iter: TokenStreamIter = input.into_iter();
74/// assert!(iter.next().is_some());
75/// assert!(iter.next().is_none());
76/// # Ok::<(), proc_macro2::LexError>(())
77/// ```
78pub type TokenStreamIter = proc_macro2::token_stream::IntoIter;
79
80/// Stores one to three operator characters inline.
81///
82/// Equality with a string compares the complete character sequence, not a prefix.
83///
84/// # Examples
85///
86/// ```
87/// use cel_parser::lex_lexer::PunctOp;
88///
89/// let op = PunctOp::Two(['&', '&']);
90/// assert!(op == "&&");
91/// assert!(op != "&");
92/// assert!(op != "&&&");
93/// ```
94#[derive(Clone, Debug)]
95pub enum PunctOp {
96    /// Single character (e.g. `+`, `-`).
97    One(char),
98    /// Two characters (e.g. `&&`, `<=`, `..`).
99    Two([char; 2]),
100    /// Three characters (e.g. `..=`).
101    Three([char; 3]),
102}
103
104impl PartialEq<str> for PunctOp {
105    /// Returns whether the string contains exactly this operator's characters.
106    fn eq(&self, other: &str) -> bool {
107        let chars: &[char] = match self {
108            PunctOp::One(c) => std::slice::from_ref(c),
109            PunctOp::Two(chars) => chars,
110            PunctOp::Three(chars) => chars,
111        };
112        chars.iter().copied().eq(other.chars())
113    }
114}
115
116impl PartialEq<&str> for PunctOp {
117    /// Returns whether the borrowed string equals this operator's characters.
118    fn eq(&self, other: &&str) -> bool {
119        self.eq(*other)
120    }
121}
122
123/// Lazily flattens token trees and combines adjacent supported punctuation.
124///
125/// Each group emits matching delimiters, including `Delimiter::None`. Operators
126/// combine only across joint punctuation within the same group. Combined tokens
127/// retain the first punctuation's span; delimiters retain their group's span.
128/// The lexer is forward-only and non-cloneable; parsers use a single-token
129/// [`std::iter::Peekable`] cursor, not indexed lookahead or speculative copies.
130///
131/// - Invariant: The group stack owns open groups in nesting order; lookahead and
132///   pending close tokens precede any further tokens from their parent iterator.
133/// - Complexity: Traversal visits O(n) token trees and retains O(d) group levels
134///   for nesting depth d, excluding token payloads and upstream iterator storage.
135///
136/// # Examples
137///
138/// ```
139/// use cel_parser::lex_lexer::{LexLexer, Token};
140///
141/// let input: proc_macro2::TokenStream = "(true)".parse()?;
142/// let mut lexer = LexLexer::new(input.into_iter());
143/// assert!(matches!(lexer.next(), Some(Token::OpenDelim { .. })));
144/// assert!(matches!(lexer.next(), Some(Token::Literal(syn::Lit::Bool(b))) if b.value));
145/// assert!(matches!(lexer.next(), Some(Token::CloseDelim { .. })));
146/// assert!(lexer.next().is_none());
147/// # Ok::<(), proc_macro2::LexError>(())
148/// ```
149pub struct LexLexer {
150    input: TokenStreamIter,
151    /// Stack of iterators for nested groups - allows lazy flattening.
152    /// Each entry tracks the iterator and its close delimiter info.
153    group_stack: Vec<GroupLevel>,
154    /// Pending close delimiter to emit when we've just exhausted a group iterator.
155    pending_close: Option<(Delimiter, Span)>,
156    /// Pending token that was consumed while looking ahead.
157    pending_token: Option<TokenTree>,
158}
159
160impl LexLexer {
161    /// Takes ownership of an iterator without consuming any tokens.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use cel_parser::lex_lexer::LexLexer;
167    ///
168    /// let lexer = LexLexer::new(proc_macro2::TokenStream::new().into_iter());
169    /// assert_eq!(lexer.count(), 0);
170    /// ```
171    pub fn new(input: TokenStreamIter) -> Self {
172        Self {
173            input,
174            group_stack: Vec::new(),
175            pending_close: None,
176            pending_token: None,
177        }
178    }
179
180    /// Parses a literal while preserving its span.
181    ///
182    /// Returns `Token::Error` if Syn cannot interpret the literal.
183    ///
184    /// - Complexity: O(n) time and space for a literal of n characters.
185    fn convert_literal(literal: proc_macro2::Literal) -> Token {
186        let span = literal.span();
187        match syn::parse2(TokenTree::Literal(literal).into()) {
188            Ok(literal) => Token::Literal(literal),
189            Err(error) => Token::Error {
190                message: error.to_string(),
191                span,
192            },
193        }
194    }
195
196    /// Converts boolean keywords to literals and preserves every other identifier.
197    ///
198    /// - Complexity: O(n) time for an identifier of n characters.
199    fn convert_identifier(ident: Ident) -> Token {
200        if ident == "true" || ident == "false" {
201            Token::Literal(Lit::Bool(syn::LitBool::new(ident == "true", ident.span())))
202        } else {
203            Token::Identifier(ident)
204        }
205    }
206
207    /// Returns whether two characters form a supported compound operator.
208    fn is_compound_operator(first: char, second: char) -> bool {
209        matches!(
210            (first, second),
211            ('&', '&')
212                | ('|', '|')
213                | ('=', '=')
214                | ('!', '=')
215                | ('<', '=')
216                | ('>', '=')
217                | ('<', '<')
218                | ('>', '>')
219                | ('-', '>')
220                | ('=', '>')
221                | (':', '=')
222                | ('.', '.')
223        )
224    }
225
226    /// Returns the next lookahead, innermost group, or root token, in that order.
227    ///
228    /// On group exhaustion, returns `None` and schedules its closing delimiter
229    /// before any further parent tokens.
230    fn next_token_tree(&mut self) -> Option<TokenTree> {
231        if let Some(token) = self.pending_token.take() {
232            return Some(token);
233        }
234
235        if let Some(level) = self.group_stack.last_mut() {
236            if let Some(tt) = level.iter.next() {
237                return Some(tt);
238            }
239            let level = self.group_stack.pop().unwrap();
240            self.pending_close = Some((level.delimiter, level.span));
241            return None;
242        }
243
244        self.input.next()
245    }
246
247    /// Consumes the longest supported operator beginning with `first`.
248    ///
249    /// Preserves the first punctuation's span and retains unmatched lookahead.
250    /// Only joint punctuation within the current group can extend the operator.
251    fn combine_punctuation(&mut self, first: proc_macro2::Punct) -> Token {
252        let ch = first.as_char();
253        let mut op = PunctOp::One(ch);
254        if first.spacing() == Spacing::Joint {
255            match self.next_token_tree() {
256                Some(TokenTree::Punct(second))
257                    if Self::is_compound_operator(ch, second.as_char()) =>
258                {
259                    let next_ch = second.as_char();
260                    op = PunctOp::Two([ch, next_ch]);
261                    if [ch, next_ch] == ['.', '.'] && second.spacing() == Spacing::Joint {
262                        match self.next_token_tree() {
263                            Some(TokenTree::Punct(third)) if third.as_char() == '=' => {
264                                op = PunctOp::Three(['.', '.', '=']);
265                            }
266                            other => self.pending_token = other,
267                        }
268                    }
269                }
270                other => self.pending_token = other,
271            }
272        }
273        Token::Punct {
274            op,
275            span: first.span(),
276        }
277    }
278
279    /// `doc_attribute_tail = ["!"] "[" "doc" "=" string_literal "]".`
280    ///
281    /// Returns a documentation token or `Token::Error` for any mismatch, consuming
282    /// the inspected token trees without rolling back.
283    ///
284    /// - Complexity: O(n) time and space for an attribute containing n characters.
285    fn parse_doc_comment_attribute(&mut self, hash_span: Span) -> Token {
286        let next = self.next_token_tree();
287        let (inner, group_token) = match next {
288            Some(TokenTree::Punct(p)) if p.as_char() == '!' => (true, self.next_token_tree()),
289            other => (false, other),
290        };
291        let group = match group_token {
292            Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => g,
293            Some(other) => {
294                return Token::Error {
295                    message: "expected `[` after `#`".to_string(),
296                    span: other.span(),
297                };
298            }
299            None => {
300                return Token::Error {
301                    message: "expected `[` after `#`".to_string(),
302                    span: hash_span,
303                };
304            }
305        };
306        let mut inner_tokens = group.stream().into_iter();
307        let ident_ok = match inner_tokens.next() {
308            Some(TokenTree::Ident(id)) => id == "doc",
309            _ => false,
310        };
311        if !ident_ok {
312            return Token::Error {
313                message: "expected `doc` after `#[`".to_string(),
314                span: group.span(),
315            };
316        }
317        let eq_ok = matches!(
318            inner_tokens.next(),
319            Some(TokenTree::Punct(p)) if p.as_char() == '='
320        );
321        if !eq_ok {
322            return Token::Error {
323                message: "expected `=` after `doc`".to_string(),
324                span: group.span(),
325            };
326        }
327        let text = match inner_tokens.next() {
328            Some(TokenTree::Literal(lit)) => {
329                let token_stream: proc_macro2::TokenStream = TokenTree::Literal(lit).into();
330                match syn::parse2::<Lit>(token_stream) {
331                    Ok(Lit::Str(s)) => s.value(),
332                    _ => {
333                        return Token::Error {
334                            message: "expected a string literal after `doc =`".to_string(),
335                            span: group.span(),
336                        };
337                    }
338                }
339            }
340            _ => {
341                return Token::Error {
342                    message: "expected a string literal after `doc =`".to_string(),
343                    span: group.span(),
344                };
345            }
346        };
347        if inner_tokens.next().is_some() {
348            return Token::Error {
349                message: "unexpected token after `doc = \"...\"`".to_string(),
350                span: group.span(),
351            };
352        }
353        Token::DocComment {
354            text,
355            inner,
356            span: hash_span,
357        }
358    }
359}
360
361/// Represents a parsed literal, including boolean keywords.
362///
363/// # Examples
364///
365/// ```
366/// use cel_parser::lex_lexer::{HasSpan, Literal};
367///
368/// let literal: Literal = syn::parse_str("true")?;
369/// assert!(matches!(literal, Literal::Bool(ref b) if b.value));
370/// let span = HasSpan::span(&literal);
371/// # Ok::<(), syn::Error>(())
372/// ```
373pub type Literal = Lit;
374
375impl HasSpan for Literal {
376    /// Returns the literal's span, including uninterpreted verbatim literals.
377    fn span(&self) -> Span {
378        syn::spanned::Spanned::span(self)
379    }
380}
381
382/// A flattened token that represents elements from a TokenTree stream.
383///
384/// Groups are flattened into OpenDelim and CloseDelim tokens, making parsing
385/// simpler by removing nesting from the token stream.
386///
387/// # Examples
388///
389/// ```
390/// use cel_parser::lex_lexer::{LexLexer, Token};
391///
392/// let input: proc_macro2::TokenStream = "name".parse()?;
393/// let token = LexLexer::new(input.into_iter()).next().unwrap();
394/// assert!(matches!(token, Token::Identifier(id) if id == "name"));
395/// # Ok::<(), proc_macro2::LexError>(())
396/// ```
397#[derive(Debug)]
398pub enum Token {
399    /// A literal value (integer, string, boolean, or float) with eager discrimination.
400    Literal(Literal),
401
402    /// An identifier.
403    Identifier(Ident),
404
405    /// A punctuation operator storing all one to three characters inline.
406    Punct {
407        /// The operator (e.g., "+", "&&", "<=").
408        op: PunctOp,
409        /// Span for error reporting.
410        span: Span,
411    },
412
413    /// Opening delimiter (flattened from Group).
414    OpenDelim {
415        /// The type of delimiter (Parenthesis, Brace, Bracket).
416        delimiter: Delimiter,
417        /// Span for error reporting.
418        span: Span,
419    },
420
421    /// Closing delimiter (flattened from Group).
422    CloseDelim {
423        /// The type of delimiter (Parenthesis, Brace, Bracket).
424        delimiter: Delimiter,
425        /// Span for error reporting.
426        span: Span,
427    },
428
429    /// A recognized `///`/`//!` doc comment, unwrapped from the `#[doc = "..."]`/
430    /// `#![doc = "..."]` attribute-shaped token sequence `proc_macro2` produces for it. General
431    /// `#[...]` attribute syntax is not supported — see `Token::Error`.
432    DocComment {
433        /// The comment's text: the doc-attribute string literal's value verbatim (including its
434        /// leading space), e.g. `" the total"` for `/// the total`.
435        text: String,
436        /// `true` for `//!`/`#![doc]` (inner), `false` for `///`/`#[doc]` (outer).
437        inner: bool,
438        /// The `#` token's span.
439        span: Span,
440    },
441
442    /// An unsupported attribute shape or a literal Syn cannot parse.
443    Error {
444        /// A description of what was expected instead.
445        message: String,
446        /// Where the mismatch was detected.
447        span: Span,
448    },
449}
450
451impl HasSpan for Token {
452    /// Returns the token's preserved source or diagnostic span.
453    fn span(&self) -> Span {
454        match self {
455            Token::Literal(lit) => lit.span(),
456            Token::Identifier(ident) => ident.span(),
457            Token::Punct { span, .. } => *span,
458            Token::OpenDelim { span, .. } => *span,
459            Token::CloseDelim { span, .. } => *span,
460            Token::DocComment { span, .. } => *span,
461            Token::Error { span, .. } => *span,
462        }
463    }
464}
465
466impl HasSpan for TokenTree {
467    /// Returns the source span of any token-tree variant.
468    fn span(&self) -> Span {
469        TokenTree::span(self)
470    }
471}
472
473impl Iterator for LexLexer {
474    type Item = Token;
475
476    /// Returns the next flattened token or `None` after exhausting the input.
477    ///
478    /// - Complexity: O(n) time and space for the consumed token payload; entering
479    ///   a group also retains its iterator and may grow the O(d) nesting stack.
480    fn next(&mut self) -> Option<Self::Item> {
481        if let Some((delimiter, span)) = self.pending_close.take() {
482            return Some(Token::CloseDelim { delimiter, span });
483        }
484
485        let token = match self.next_token_tree() {
486            Some(token) => token,
487            None => {
488                return self
489                    .pending_close
490                    .take()
491                    .map(|(delimiter, span)| Token::CloseDelim { delimiter, span });
492            }
493        };
494
495        Some(match token {
496            TokenTree::Group(group) => {
497                let delimiter = group.delimiter();
498                let span = group.span();
499                self.group_stack.push(GroupLevel {
500                    iter: group.stream().into_iter(),
501                    delimiter,
502                    span,
503                });
504                Token::OpenDelim { delimiter, span }
505            }
506            TokenTree::Punct(punct) if punct.as_char() == '#' => {
507                self.parse_doc_comment_attribute(punct.span())
508            }
509            TokenTree::Punct(punct) => self.combine_punctuation(punct),
510            TokenTree::Literal(literal) => Self::convert_literal(literal),
511            TokenTree::Ident(ident) => Self::convert_identifier(ident),
512        })
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use proc_macro2::TokenStream;
520    use std::str::FromStr;
521
522    /// Compares complete character sequences, including non-ASCII characters.
523    #[test]
524    fn punctuation_equality_compares_characters_without_prefix_matches() {
525        let cases = [
526            (PunctOp::One('+'), "+", true),
527            (PunctOp::One('+'), "++", false),
528            (PunctOp::One('+'), "", false),
529            (PunctOp::One('\u{a7}'), "\u{a7}", true),
530            (PunctOp::Two(['&', '&']), "&&", true),
531            (PunctOp::Two(['&', '&']), "&", false),
532            (PunctOp::Two(['&', '&']), "&&&", false),
533            (PunctOp::Two(['&', '&']), "&|", false),
534            (PunctOp::Three(['.', '.', '=']), "..=", true),
535            (PunctOp::Three(['.', '.', '=']), "..", false),
536            (PunctOp::Three(['.', '.', '=']), "..==", false),
537            (PunctOp::Three(['.', '.', '=']), "...", false),
538        ];
539        // Check both supported string comparison interfaces against the same contract.
540        for (op, text, expected) in cases {
541            assert_eq!(op == *text, expected, "{op:?} versus {text:?}");
542            assert_eq!(op == text, expected, "{op:?} versus {text:?}");
543        }
544    }
545
546    /// Preserves the source span of a literal not interpreted by Syn.
547    #[test]
548    fn verbatim_literal_retains_its_span() {
549        let literal = proc_macro2::Literal::from_str("42").unwrap();
550        let literal = Lit::Verbatim(literal);
551        assert_eq!(HasSpan::span(&literal).source_text().as_deref(), Some("42"));
552    }
553
554    /// Clones punctuation values without sharing mutable character storage.
555    #[test]
556    fn punctuation_clones_are_independent_values() {
557        let cases = [
558            (PunctOp::One('+'), "+"),
559            (PunctOp::Two(['&', '&']), "&&"),
560            (PunctOp::Three(['.', '.', '=']), "..="),
561        ];
562        // Mutate each original and its copy independently through their public variants.
563        for (mut original, text) in cases {
564            let mut copied = original.clone();
565            assert!(copied == text);
566            match &mut original {
567                PunctOp::One(c) => *c = '!',
568                PunctOp::Two(chars) => chars[0] = '!',
569                PunctOp::Three(chars) => chars[0] = '!',
570            }
571            assert!(copied == text);
572            copied = PunctOp::One('?');
573            assert!(copied == "?");
574            assert!(original != "?");
575            assert!(original != text);
576        }
577    }
578
579    /// Preserves literal values and spans across every supported literal category.
580    #[test]
581    fn literal_spans_preserve_the_source() {
582        // All literal variants use the same source-span contract.
583        for source in [
584            "42",
585            "1.5",
586            "\"text\"",
587            "b\"bytes\"",
588            "c\"cstr\"",
589            "'x'",
590            "b'x'",
591            "true",
592            "false",
593        ] {
594            let mut lexer = LexLexer::new(TokenStream::from_str(source).unwrap().into_iter());
595            let Some(Token::Literal(literal)) = lexer.next() else {
596                panic!("expected literal for {source}");
597            };
598            assert_eq!(
599                HasSpan::span(&literal).source_text().as_deref(),
600                Some(source)
601            );
602            assert_eq!(
603                HasSpan::span(&Token::Literal(literal))
604                    .source_text()
605                    .as_deref(),
606                Some(source)
607            );
608            assert!(lexer.next().is_none());
609        }
610    }
611
612    /// Preserves token-tree spans for groups, identifiers, punctuation, and literals.
613    #[test]
614    fn token_tree_spans_cover_each_variant() {
615        // Verify delegation for every token-tree variant.
616        for source in ["(x)", "name", "+", "42"] {
617            let tree = TokenStream::from_str(source)
618                .unwrap()
619                .into_iter()
620                .next()
621                .unwrap();
622            assert_eq!(HasSpan::span(&tree).source_text().as_deref(), Some(source));
623        }
624    }
625
626    /// Preserves diagnostic and delimiter spans rather than substituting call-site spans.
627    #[test]
628    fn flattened_tokens_preserve_their_associated_spans() {
629        let cases: &[(&str, &[&str])] = &[
630            ("(x)", &["(x)", "x", "(x)"]),
631            ("&&", &["&"]),
632            ("#[bad]", &["[bad]"]),
633        ];
634        // Group delimiters use group spans; combined punctuation uses its first character.
635        for &(source, spans) in cases {
636            let mut lexer = LexLexer::new(TokenStream::from_str(source).unwrap().into_iter());
637            for &expected in spans {
638                let token = lexer.next().unwrap();
639                assert_eq!(
640                    HasSpan::span(&token).source_text().as_deref(),
641                    Some(expected)
642                );
643            }
644            assert!(lexer.next().is_none());
645        }
646    }
647
648    /// Produces no tokens for empty input, including repeated exhaustion checks.
649    #[test]
650    fn empty_input_remains_exhausted() {
651        let mut lexer = LexLexer::new(TokenStream::new().into_iter());
652        assert!(lexer.next().is_none());
653        assert!(lexer.next().is_none());
654    }
655
656    /// Combines exactly the supported adjacent operators.
657    #[test]
658    fn combines_each_supported_operator() {
659        // Each operator must consume its complete spelling, but not the following identifier.
660        for text in [
661            "&&", "||", "==", "!=", "<=", ">=", "<<", ">>", "->", "=>", ":=", "..", "..=",
662        ] {
663            let input = TokenStream::from_str(&format!("{text} tail")).unwrap();
664            let mut lexer = LexLexer::new(input.into_iter());
665            assert!(matches!(lexer.next(), Some(Token::Punct { op, .. }) if op == text));
666            assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "tail"));
667            assert!(lexer.next().is_none());
668        }
669    }
670
671    /// Preserves uncombined lookahead tokens and respects punctuation spacing.
672    #[test]
673    fn punctuation_lookahead_preserves_the_remaining_stream() {
674        let cases: &[(&str, &[&str])] = &[
675            ("& &", &["&", "&"]),
676            (".. =", &["..", "="]),
677            ("..+", &["..", "+"]),
678            ("&&=", &["&&", "="]),
679            ("+=", &["+", "="]),
680            ("...", &["..", "."]),
681        ];
682        // Compare the entire punctuation stream so no lookahead token can disappear.
683        for &(input, expected) in cases {
684            let mut lexer = LexLexer::new(TokenStream::from_str(input).unwrap().into_iter());
685            for &text in expected {
686                assert!(matches!(lexer.next(), Some(Token::Punct { op, .. }) if op == text));
687            }
688            assert!(lexer.next().is_none(), "{input}");
689        }
690    }
691
692    /// Emits every empty group's matching delimiters before resuming its parent.
693    #[test]
694    fn empty_groups_preserve_delimiter_order() {
695        let mut lexer = LexLexer::new(TokenStream::from_str("([]{}) tail").unwrap().into_iter());
696        let expected = [
697            (true, Delimiter::Parenthesis),
698            (true, Delimiter::Bracket),
699            (false, Delimiter::Bracket),
700            (true, Delimiter::Brace),
701            (false, Delimiter::Brace),
702            (false, Delimiter::Parenthesis),
703        ];
704        // Verify both delimiter kind and nesting order.
705        for (open, expected_delimiter) in expected {
706            match lexer.next() {
707                Some(Token::OpenDelim { delimiter, .. }) if open => {
708                    assert_eq!(delimiter, expected_delimiter);
709                }
710                Some(Token::CloseDelim { delimiter, .. }) if !open => {
711                    assert_eq!(delimiter, expected_delimiter);
712                }
713                other => panic!("expected delimiter, got {other:?}"),
714            }
715        }
716        assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "tail"));
717        assert!(lexer.next().is_none());
718    }
719
720    /// Emits a group close after punctuation lookahead or an incomplete attribute.
721    #[test]
722    fn lookahead_does_not_consume_a_group_close() {
723        // Joint punctuation is constructible even when no following token exists.
724        for punctuation in ["&", "..", "#", "#!"] {
725            let group = proc_macro2::Group::new(
726                Delimiter::Parenthesis,
727                punctuation
728                    .chars()
729                    .map(|ch| TokenTree::Punct(proc_macro2::Punct::new(ch, Spacing::Joint)))
730                    .collect(),
731            );
732            let input: TokenStream = [
733                TokenTree::Group(group),
734                TokenTree::Ident(Ident::new("tail", Span::call_site())),
735            ]
736            .into_iter()
737            .collect();
738            let mut lexer = LexLexer::new(input.into_iter());
739            assert!(matches!(lexer.next(), Some(Token::OpenDelim { .. })));
740            match lexer.next() {
741                Some(Token::Punct { op, .. }) if !punctuation.starts_with('#') => {
742                    assert!(op == punctuation);
743                }
744                Some(Token::Error { message, .. }) if punctuation.starts_with('#') => {
745                    assert!(!message.is_empty());
746                }
747                other => panic!("expected punctuation or attribute error, got {other:?}"),
748            }
749            assert!(matches!(lexer.next(), Some(Token::CloseDelim { .. })));
750            assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "tail"));
751            assert!(lexer.next().is_none());
752        }
753    }
754
755    /// Emits delimiters for invisible groups without combining across their boundaries.
756    #[test]
757    fn invisible_groups_bound_operator_combining() {
758        let contents = [
759            TokenTree::Punct(proc_macro2::Punct::new('.', Spacing::Joint)),
760            TokenTree::Punct(proc_macro2::Punct::new('.', Spacing::Joint)),
761        ]
762        .into_iter()
763        .collect();
764        let input: TokenStream = [
765            TokenTree::Group(proc_macro2::Group::new(Delimiter::None, contents)),
766            TokenTree::Punct(proc_macro2::Punct::new('=', Spacing::Alone)),
767        ]
768        .into_iter()
769        .collect();
770        let mut lexer = LexLexer::new(input.into_iter());
771        assert!(matches!(
772            lexer.next(),
773            Some(Token::OpenDelim {
774                delimiter: Delimiter::None,
775                ..
776            })
777        ));
778        assert!(matches!(lexer.next(), Some(Token::Punct { op, .. }) if op == ".."));
779        assert!(matches!(
780            lexer.next(),
781            Some(Token::CloseDelim {
782                delimiter: Delimiter::None,
783                ..
784            })
785        ));
786        assert!(matches!(lexer.next(), Some(Token::Punct { op, .. }) if op == "="));
787        assert!(lexer.next().is_none());
788    }
789
790    /// Reports incomplete attributes at end of input without terminating iteration early.
791    #[test]
792    fn incomplete_attributes_at_end_of_input_report_errors() {
793        // Neither form supplies the required bracket group.
794        for source in ["#", "#!"] {
795            let mut lexer = LexLexer::new(TokenStream::from_str(source).unwrap().into_iter());
796            assert!(
797                matches!(lexer.next(), Some(Token::Error { message, .. }) if !message.is_empty())
798            );
799            assert!(lexer.next().is_none());
800            assert!(lexer.next().is_none());
801        }
802    }
803
804    /// Decodes explicit documentation strings while preserving the inner marker.
805    #[test]
806    fn explicit_documentation_attributes_decode_string_values() {
807        let input =
808            TokenStream::from_str(r##"#[doc = "line\nnext"] #![doc = r#" raw "#]"##).unwrap();
809        let mut lexer = LexLexer::new(input.into_iter());
810        assert!(
811            matches!(lexer.next(), Some(Token::DocComment { text, inner: false, .. })
812                if text == "line\nnext")
813        );
814        assert!(
815            matches!(lexer.next(), Some(Token::DocComment { text, inner: true, .. })
816                if text == " raw ")
817        );
818        assert!(lexer.next().is_none());
819    }
820
821    /// Reports each malformed attribute shape without losing the next outer token.
822    #[test]
823    fn malformed_attributes_report_errors_and_resume() {
824        // Attribute contents are untrusted input, not caller preconditions.
825        for input in [
826            "#[] tail",
827            "#[doc] tail",
828            "#[doc \"x\"] tail",
829            "#[doc =] tail",
830            "#[doc = value] tail",
831            "#[doc = b\"x\"] tail",
832            "#![doc = 1] tail",
833            "#[doc = \"x\" extra] tail",
834        ] {
835            let mut lexer = LexLexer::new(TokenStream::from_str(input).unwrap().into_iter());
836            assert!(
837                matches!(lexer.next(), Some(Token::Error { message, .. }) if !message.is_empty()),
838                "{input}"
839            );
840            assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "tail"));
841            assert!(lexer.next().is_none());
842        }
843    }
844
845    /// Recognizes booleans without treating raw or longer identifiers as literals.
846    #[test]
847    fn boolean_recognition_preserves_other_identifiers() {
848        let input = TokenStream::from_str("true false r#true true_value").unwrap();
849        let mut lexer = LexLexer::new(input.into_iter());
850        assert!(matches!(lexer.next(), Some(Token::Literal(Lit::Bool(lit))) if lit.value));
851        assert!(matches!(lexer.next(), Some(Token::Literal(Lit::Bool(lit))) if !lit.value));
852        assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "r#true"));
853        assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "true_value"));
854        assert!(lexer.next().is_none());
855    }
856
857    /// Recognizes an integer literal.
858    #[test]
859    fn test_literal_integer() {
860        let input = TokenStream::from_str("42").unwrap();
861        let mut lexer = LexLexer::new(input.into_iter());
862
863        let token = lexer.next().unwrap();
864        match token {
865            Token::Literal(Lit::Int(..)) => {}
866            _ => panic!("Expected integer literal, got {:?}", token),
867        }
868    }
869
870    /// Recognizes a string literal.
871    #[test]
872    fn test_literal_string() {
873        let input = TokenStream::from_str(r#""hello""#).unwrap();
874        let mut lexer = LexLexer::new(input.into_iter());
875
876        let token = lexer.next().unwrap();
877        match token {
878            Token::Literal(Lit::Str(..)) => {}
879            _ => panic!("Expected string literal"),
880        }
881    }
882
883    /// Recognizes both boolean keyword values.
884    #[test]
885    fn test_literal_boolean() {
886        // Test 'true' boolean literal
887        let input = TokenStream::from_str("true").unwrap();
888        let mut lexer = LexLexer::new(input.into_iter());
889
890        let token = lexer.next().unwrap();
891        match token {
892            Token::Literal(Lit::Bool(lit_bool)) => {
893                assert!(lit_bool.value);
894            }
895            _ => panic!("Expected boolean literal for 'true', got {:?}", token),
896        }
897
898        // Test 'false' boolean literal
899        let input = TokenStream::from_str("false").unwrap();
900        let mut lexer = LexLexer::new(input.into_iter());
901
902        let token = lexer.next().unwrap();
903        match token {
904            Token::Literal(Lit::Bool(lit_bool)) => {
905                assert!(!lit_bool.value);
906            }
907            _ => panic!("Expected boolean literal for 'false', got {:?}", token),
908        }
909    }
910
911    /// Recognizes a floating-point literal.
912    #[test]
913    fn test_literal_float() {
914        let input = TokenStream::from_str("3.14").unwrap();
915        let mut lexer = LexLexer::new(input.into_iter());
916
917        let token = lexer.next().unwrap();
918        match token {
919            Token::Literal(Lit::Float(..)) => {}
920            _ => panic!("Expected float literal"),
921        }
922    }
923
924    /// Preserves an identifier's spelling.
925    #[test]
926    fn test_identifier() {
927        let input = TokenStream::from_str("foo").unwrap();
928        let mut lexer = LexLexer::new(input.into_iter());
929
930        let token = lexer.next().unwrap();
931        match token {
932            Token::Identifier(ident) => {
933                assert_eq!(ident.to_string(), "foo");
934            }
935            _ => panic!("Expected identifier"),
936        }
937    }
938
939    /// Emits a single punctuation character.
940    #[test]
941    fn test_punct() {
942        let input = TokenStream::from_str("+").unwrap();
943        let mut lexer = LexLexer::new(input.into_iter());
944
945        let token = lexer.next().unwrap();
946        match token {
947            Token::Punct { op, .. } => {
948                assert!(op == "+");
949            }
950            _ => panic!("Expected punctuation"),
951        }
952    }
953
954    /// Combines a logical operator between identifiers.
955    #[test]
956    fn test_compound_operator() {
957        let input = TokenStream::from_str("a && b").unwrap();
958        let lexer = LexLexer::new(input.into_iter());
959
960        let tokens: Vec<_> = lexer.collect();
961        assert_eq!(tokens.len(), 3);
962
963        match &tokens[1] {
964            Token::Punct { op, .. } => {
965                assert!(op == "&&");
966            }
967            _ => panic!("Expected && operator"),
968        }
969    }
970
971    /// Emits matching delimiters around a group's flattened contents.
972    #[test]
973    fn test_group_flattening() {
974        let input = TokenStream::from_str("(10 + 20)").unwrap();
975        let lexer = LexLexer::new(input.into_iter());
976
977        // Should get: OpenDelim, Integer, Punct, Integer, CloseDelim
978        let tokens: Vec<_> = lexer.collect();
979        assert_eq!(tokens.len(), 5);
980
981        assert!(matches!(
982            tokens[0],
983            Token::OpenDelim {
984                delimiter: Delimiter::Parenthesis,
985                ..
986            }
987        ));
988        assert!(matches!(tokens[1], Token::Literal(Lit::Int(..))));
989        assert!(matches!(&tokens[2], Token::Punct { op, .. } if op == "+"));
990        assert!(matches!(tokens[3], Token::Literal(Lit::Int(..))));
991        assert!(matches!(
992            tokens[4],
993            Token::CloseDelim {
994                delimiter: Delimiter::Parenthesis,
995                ..
996            }
997        ));
998    }
999
1000    /// Preserves the nesting order of groups and their contents.
1001    #[test]
1002    fn test_nested_groups() {
1003        let input = TokenStream::from_str("(10 + (20 * 30))").unwrap();
1004        let lexer = LexLexer::new(input.into_iter());
1005
1006        let tokens: Vec<_> = lexer.collect();
1007
1008        // Should have: OpenDelim, 10, +, OpenDelim, 20, *, 30, CloseDelim, CloseDelim
1009        assert_eq!(tokens.len(), 9);
1010
1011        // Verify structure
1012        assert!(matches!(tokens[0], Token::OpenDelim { .. }));
1013        assert!(matches!(tokens[1], Token::Literal(Lit::Int(..))));
1014        assert!(matches!(&tokens[2], Token::Punct { op, .. } if op == "+"));
1015        assert!(matches!(tokens[3], Token::OpenDelim { .. }));
1016        assert!(matches!(tokens[4], Token::Literal(Lit::Int(..))));
1017        assert!(matches!(&tokens[5], Token::Punct { op, .. } if op == "*"));
1018        assert!(matches!(tokens[6], Token::Literal(Lit::Int(..))));
1019        assert!(matches!(tokens[7], Token::CloseDelim { .. }));
1020        assert!(matches!(tokens[8], Token::CloseDelim { .. }));
1021    }
1022
1023    /// Retains an identifier's source span.
1024    #[test]
1025    fn test_span_preservation() {
1026        let input = TokenStream::from_str("foo").unwrap();
1027        let mut lexer = LexLexer::new(input.into_iter());
1028
1029        let token = lexer.next().unwrap();
1030
1031        // HasSpan trait should provide span
1032        let span = HasSpan::span(&token);
1033        assert!(!span.source_text().unwrap_or_default().is_empty());
1034    }
1035
1036    /// Preserves the order of identifiers, punctuation, and literals.
1037    #[test]
1038    fn test_mixed_tokens() {
1039        let input = TokenStream::from_str("foo + 42").unwrap();
1040        let lexer = LexLexer::new(input.into_iter());
1041
1042        let tokens: Vec<_> = lexer.collect();
1043        assert_eq!(tokens.len(), 3);
1044
1045        assert!(matches!(tokens[0], Token::Identifier(_)));
1046        assert!(matches!(&tokens[1], Token::Punct { op, .. } if op == "+"));
1047        assert!(matches!(tokens[2], Token::Literal(Lit::Int(..))));
1048    }
1049
1050    /// Emits an arrow as one operator.
1051    #[test]
1052    fn arrow_is_two_char_punct() {
1053        let stream: TokenStream = "->".parse().unwrap();
1054        let mut lexer = LexLexer::new(stream.into_iter());
1055        let tok = lexer.next().expect("one token");
1056        assert!(
1057            matches!(
1058                tok,
1059                Token::Punct {
1060                    op: PunctOp::Two(['-', '>']),
1061                    ..
1062                }
1063            ),
1064            "expected PunctOp::Two([\"->\"])), got {tok:?}"
1065        );
1066        assert!(lexer.next().is_none(), "expected no more tokens");
1067    }
1068
1069    /// Emits a fat arrow as one operator.
1070    #[test]
1071    fn fat_arrow_is_two_char_punct() {
1072        let stream: TokenStream = "=>".parse().unwrap();
1073        let mut lexer = LexLexer::new(stream.into_iter());
1074        let tok = lexer.next().expect("one token");
1075        assert!(
1076            matches!(
1077                tok,
1078                Token::Punct {
1079                    op: PunctOp::Two(['=', '>']),
1080                    ..
1081                }
1082            ),
1083            "expected PunctOp::Two([\"=>\"]), got {tok:?}"
1084        );
1085        assert!(lexer.next().is_none(), "expected no more tokens");
1086    }
1087
1088    /// Preserves outer documentation text and its outer marker.
1089    #[test]
1090    fn recognizes_an_outer_doc_comment() {
1091        let input = TokenStream::from_str("/// the total").unwrap();
1092        let mut lexer = LexLexer::new(input.into_iter());
1093        let token = lexer.next().unwrap();
1094        match token {
1095            Token::DocComment { text, inner, .. } => {
1096                assert_eq!(text, " the total");
1097                assert!(!inner);
1098            }
1099            other => panic!("expected DocComment, got {other:?}"),
1100        }
1101        assert!(lexer.next().is_none());
1102    }
1103
1104    /// Preserves inner documentation text and its inner marker.
1105    #[test]
1106    fn recognizes_an_inner_doc_comment() {
1107        let input = TokenStream::from_str("//! module docs").unwrap();
1108        let mut lexer = LexLexer::new(input.into_iter());
1109        let token = lexer.next().unwrap();
1110        match token {
1111            Token::DocComment { text, inner, .. } => {
1112                assert_eq!(text, " module docs");
1113                assert!(inner);
1114            }
1115            other => panic!("expected DocComment, got {other:?}"),
1116        }
1117    }
1118
1119    /// Resumes ordinary tokenization after documentation.
1120    #[test]
1121    fn doc_comment_run_is_followed_by_the_next_real_token() {
1122        let input = TokenStream::from_str("/// x\ncell").unwrap();
1123        let mut lexer = LexLexer::new(input.into_iter());
1124        assert!(matches!(lexer.next(), Some(Token::DocComment { .. })));
1125        match lexer.next() {
1126            Some(Token::Identifier(ident)) => assert_eq!(ident.to_string(), "cell"),
1127            other => panic!("expected Identifier(cell), got {other:?}"),
1128        }
1129    }
1130
1131    /// Rejects a non-documentation attribute.
1132    #[test]
1133    fn a_non_doc_attribute_is_a_lex_error() {
1134        let input = TokenStream::from_str("#[foo]").unwrap();
1135        let mut lexer = LexLexer::new(input.into_iter());
1136        let token = lexer.next().unwrap();
1137        assert!(
1138            matches!(token, Token::Error { .. }),
1139            "expected Error, got {token:?}"
1140        );
1141    }
1142
1143    /// Rejects a numeric documentation value.
1144    #[test]
1145    fn a_non_string_doc_value_is_a_lex_error() {
1146        let input = TokenStream::from_str("#[doc = 5]").unwrap();
1147        let mut lexer = LexLexer::new(input.into_iter());
1148        let token = lexer.next().unwrap();
1149        assert!(
1150            matches!(token, Token::Error { .. }),
1151            "expected Error, got {token:?}"
1152        );
1153    }
1154
1155    /// Rejects an attribute marker followed by an identifier instead of a group.
1156    #[test]
1157    fn a_bare_hash_with_no_group_is_a_lex_error() {
1158        let input = TokenStream::from_str("# foo").unwrap();
1159        let mut lexer = LexLexer::new(input.into_iter());
1160        let token = lexer.next().unwrap();
1161        assert!(
1162            matches!(token, Token::Error { .. }),
1163            "expected Error, got {token:?}"
1164        );
1165    }
1166
1167    /// Rejects an attribute group with parentheses instead of brackets.
1168    #[test]
1169    fn a_hash_led_group_with_the_wrong_delimiter_is_a_lex_error() {
1170        let input = TokenStream::from_str("#(foo)").unwrap();
1171        let mut lexer = LexLexer::new(input.into_iter());
1172        let token = lexer.next().unwrap();
1173        assert!(
1174            matches!(token, Token::Error { .. }),
1175            "expected Error, got {token:?}"
1176        );
1177    }
1178
1179    /// Rejects trailing tokens in a documentation attribute.
1180    #[test]
1181    fn extra_tokens_after_the_doc_string_are_a_lex_error() {
1182        let input = TokenStream::from_str("#[doc = \"x\", extra]").unwrap();
1183        let mut lexer = LexLexer::new(input.into_iter());
1184        let token = lexer.next().unwrap();
1185        assert!(
1186            matches!(token, Token::Error { .. }),
1187            "expected Error, got {token:?}"
1188        );
1189    }
1190
1191    /// Retains a documentation comment's source span.
1192    #[test]
1193    fn doc_comment_token_has_a_span() {
1194        let input = TokenStream::from_str("/// x").unwrap();
1195        let mut lexer = LexLexer::new(input.into_iter());
1196        let token = lexer.next().unwrap();
1197        let span = HasSpan::span(&token);
1198        assert!(!span.source_text().unwrap_or_default().is_empty());
1199    }
1200
1201    /// Emits the assignment operator as one token.
1202    #[test]
1203    fn walrus_operator_lexes_as_one_compound_token() {
1204        let stream: TokenStream = ":=".parse().unwrap();
1205        let mut lexer = LexLexer::new(stream.into_iter());
1206        let token = lexer.next().expect("one token");
1207        assert!(matches!(
1208            token,
1209            Token::Punct { ref op, .. } if op == ":="
1210        ));
1211        assert!(lexer.next().is_none());
1212    }
1213
1214    /// Preserves an identifier following a lone colon.
1215    #[test]
1216    fn colon_followed_by_non_equals_stays_two_separate_tokens() {
1217        let stream: TokenStream = ": x".parse().unwrap();
1218        let mut lexer = LexLexer::new(stream.into_iter());
1219        let first = lexer.next().expect("colon token");
1220        assert!(matches!(first, Token::Punct { ref op, .. } if op == ":"));
1221        assert!(matches!(lexer.next(), Some(Token::Identifier(id)) if id == "x"));
1222        assert!(lexer.next().is_none());
1223    }
1224
1225    /// Keeps the endpoints of a range as integer literals.
1226    #[test]
1227    fn digit_followed_by_double_dot_does_not_lex_as_a_float_literal() {
1228        let input = TokenStream::from_str("5..10").unwrap();
1229        let mut lexer = LexLexer::new(input.into_iter());
1230        match lexer.next() {
1231            Some(Token::Literal(Lit::Int(lit))) => {
1232                assert_eq!(lit.base10_parse::<i32>().unwrap(), 5)
1233            }
1234            other => panic!("expected an integer literal, got {other:?}"),
1235        }
1236        assert!(matches!(lexer.next(), Some(Token::Punct { op, .. }) if op == ".."));
1237        assert!(matches!(lexer.next(), Some(Token::Literal(Lit::Int(lit)))
1238                if lit.base10_parse::<i32>().unwrap() == 10));
1239        assert!(lexer.next().is_none());
1240    }
1241
1242    /// Emits an exclusive range operator as one token.
1243    #[test]
1244    fn double_dot_combines_into_two_char_punct() {
1245        let stream: TokenStream = "..".parse().unwrap();
1246        let mut lexer = LexLexer::new(stream.into_iter());
1247        let tok = lexer.next().expect("one token");
1248        assert!(
1249            matches!(
1250                tok,
1251                Token::Punct {
1252                    op: PunctOp::Two(['.', '.']),
1253                    ..
1254                }
1255            ),
1256            "expected PunctOp::Two(['.', '.']), got {tok:?}"
1257        );
1258        assert!(lexer.next().is_none(), "expected no more tokens");
1259    }
1260
1261    /// Emits an inclusive range operator as one token.
1262    #[test]
1263    fn double_dot_equals_combines_into_three_char_punct() {
1264        let stream: TokenStream = "..=".parse().unwrap();
1265        let mut lexer = LexLexer::new(stream.into_iter());
1266        let tok = lexer.next().expect("one token");
1267        assert!(
1268            matches!(
1269                tok,
1270                Token::Punct {
1271                    op: PunctOp::Three(['.', '.', '=']),
1272                    ..
1273                }
1274            ),
1275            "expected PunctOp::Three(['.', '.', '=']), got {tok:?}"
1276        );
1277        assert!(lexer.next().is_none(), "expected no more tokens");
1278    }
1279
1280    /// Preserves the literal after an exclusive range operator.
1281    #[test]
1282    fn double_dot_not_followed_by_equals_stays_two_char_and_does_not_consume_the_next_token() {
1283        let stream: TokenStream = "..5".parse().unwrap();
1284        let mut lexer = LexLexer::new(stream.into_iter());
1285        let first = lexer.next().expect("dot-dot token");
1286        assert!(
1287            matches!(
1288                first,
1289                Token::Punct {
1290                    op: PunctOp::Two(['.', '.']),
1291                    ..
1292                }
1293            ),
1294            "expected PunctOp::Two(['.', '.']), got {first:?}"
1295        );
1296        match lexer.next() {
1297            Some(Token::Literal(Lit::Int(lit))) => {
1298                assert_eq!(lit.base10_parse::<i32>().unwrap(), 5)
1299            }
1300            other => panic!("expected integer literal 5, got {other:?}"),
1301        }
1302    }
1303
1304    /// Preserves an integer field index after a single dot.
1305    #[test]
1306    fn single_dot_before_a_digit_is_unaffected_by_range_combining() {
1307        // Regression guard: tuple field access (`x.0`) must still lex as a lone `.` Punct
1308        // followed by an integer literal, not get swept into the new `..`/`..=` combining.
1309        let stream: TokenStream = ".0".parse().unwrap();
1310        let mut lexer = LexLexer::new(stream.into_iter());
1311        let first = lexer.next().expect("dot token");
1312        assert!(
1313            matches!(
1314                first,
1315                Token::Punct {
1316                    op: PunctOp::One('.'),
1317                    ..
1318                }
1319            ),
1320            "expected PunctOp::One('.'), got {first:?}"
1321        );
1322        assert!(matches!(lexer.next(), Some(Token::Literal(Lit::Int(lit)))
1323                if lit.base10_parse::<i32>().unwrap() == 0));
1324        assert!(lexer.next().is_none());
1325    }
1326}