compose_syntax/
test_utils.rs

1use crate::{Lexer, Span, SyntaxError, SyntaxNode};
2use crate::file::FileId;
3use crate::kind::SyntaxKind;
4use compose_error_codes::ErrorCode;
5use extension_traits::extension;
6use std::num::NonZeroU16;
7use std::ops::Range;
8
9/// Creates a non-interned file id for testing.
10pub const fn test_file_id() -> FileId {
11    FileId::from_raw(NonZeroU16::new(1).unwrap())
12}
13
14pub struct AstTester {
15    pub nodes: Vec<SyntaxNode>,
16    pub pos: usize,
17}
18
19impl AstTester {
20    #[track_caller]
21    pub fn new(code: &str) -> Self {
22        let p = assert_parse(code);
23        Self {
24            nodes: p.nodes,
25            pos: 0,
26        }
27    }
28}
29
30
31
32#[macro_export]
33macro_rules! assert_ast {
34    // Entry point: Parse then pass along to inner calls
35    ($code:expr, $($tt:tt)*) => {{
36        let mut p = $crate::test_utils::AstTester::new($code);
37        $crate::test_utils::assert_ast!(@top p, $($tt)*);
38    }};
39
40    // Top level binding: Type as var { ... }
41    (@top $tester:ident, $var:ident as $ty:ty { $($inner:tt)* } $($rest:tt)*) => {
42        #[allow(unused)]
43        use $crate::ast::AstNode;
44        let idx = $tester.pos;
45        $tester.pos += 1 ;
46        let node = &$tester.nodes[idx];
47        let $var = node
48            .cast::<$ty>()
49            .expect(&format!("failed to cast to {} at {idx}, got: {:?}", stringify!($ty), node.kind()));
50
51        $crate::assert_ast!(@seq &idx.to_string(), $($inner)*);
52
53        $crate::assert_ast!(@top $tester, $($rest)*);
54    };
55
56    // discard the rest of the top level nodes
57    (@top $tester:ident, ...) => {
58        $tester.pos = $tester.nodes.len();
59    };
60
61    // End of top level
62    (@top $tester:ident, ) => {
63        assert_eq!($tester.pos, $tester.nodes.len(), "Not all nodes were consumed: {:#?}", &$tester.nodes[$tester.pos..]);
64    };
65
66    // Sequence: iterable expression => [ ... ]
67    (@seq $path:expr, $expr:expr => [ $($inner:tt)* ] $($rest:tt)*) => {
68        let mut iter = $expr.enumerate();
69        $crate::assert_ast!(@loop $path, iter, $($inner)*);
70        $crate::assert_ast!(@seq $path, $($rest)*);
71    };
72
73    // Sequence: with new casted variable binding: with var: Type => { ... }
74    (@seq $path:expr, with $inner_var:ident: $ty:ty = $expr:expr => { $($inner:tt)* } $($rest:tt)*) => {
75        {
76        let $inner_var = $expr;
77        let $inner_var = $inner_var.cast::<$ty>().expect(&format!("failed to cast to {} at {}", stringify!($ty), $path));
78        let _sub_path = format!("{}.{}", $path, stringify!($inner_var));
79        $crate::assert_ast!(@seq &_sub_path, $($inner)*);
80        }
81        $crate::assert_ast!(@seq $path, $($rest)*);
82    };
83
84    // Expression within a node
85    (@seq $path:expr, $expr:expr; $($rest:tt)*) => {{
86        $expr;
87        $crate::assert_ast!(@seq $path, $($rest)*);
88    }};
89
90    // Statement within a node
91    (@seq $path:expr, $stmt:stmt; $($rest:tt)*) => {{
92        $stmt
93        $crate::assert_ast!(@seq $path, $($rest)*);
94    }};
95
96    // End of sequence
97    (@seq $path:expr,) => {};
98
99    // Loop element (within an iterable expression): Type as var { ... }
100    (@loop $path:expr, $iter:ident, $var:ident as $ty:ty { $($inner:tt)* } $($rest:tt)*) => {
101        {
102            let _item = $iter.next().expect("failed to get next value");
103            let idx = _item.0;
104            let _sub_path = format!("{}.{}", $path, idx);
105            let $var = _item.1.cast::<$ty>().expect(&format!("failed to cast to {}", stringify!($ty)));
106            $crate::assert_ast!(@seq &_sub_path, $($inner)*);
107        }
108
109        $crate::assert_ast!(@loop $path, $iter, $($rest)*);
110    };
111
112    // discard rest of the items
113    (@loop $path:expr, $iter:ident, ...) => {
114        // drain the iter
115        while let Some(_) = $iter.next() { }
116    };
117
118    // end loop
119    (@loop $path:expr, $iter:ident,) => {
120        assert_eq!($iter.next().is_none(), true, "Not all values were consumed at {}", $path);
121    };
122
123
124    // --------------
125    // error handlers
126    // --------------
127
128    (@seq $path:expr, with $(rest:tt)*) => {
129        compile_error!("with usage: with <var_name>: <type> = <expr> => { ... }")
130    };
131
132    (@loop, $path:expr, $iter:ident, $(rest:tt)*) => {
133        compile_error!("loop usage: <type> as <var> { ... }")
134    };
135}
136
137pub use assert_ast;
138
139
140pub fn test_parse(code: &str) -> Vec<SyntaxNode> {
141    let file_id = FileId::new("main.comp");
142    let nodes = crate::parse(code, file_id);
143    nodes
144}
145
146#[track_caller]
147pub fn assert_parse(code: &str) -> NodesTester {
148    let nodes = test_parse(code);
149
150    let errors = nodes
151        .iter()
152        .flat_map(|node| node.errors())
153        .map(|error| error.code)
154        .collect::<Vec<_>>();
155
156    assert_eq!(errors, vec![]);
157
158    NodesTester::new(nodes)
159}
160
161pub fn assert_parse_with_warnings(code: &str, expected_warnings: &[ErrorCode]) -> NodesTester {
162    let nodes = test_parse(code);
163    let actual = nodes
164        .iter()
165        .flat_map(|node| node.warnings())
166        .map(|warning| warning.code)
167        .collect::<Vec<_>>();
168
169    assert_eq!(actual.len(), expected_warnings.len());
170
171    for (actual, expected) in actual.iter().zip(expected_warnings.iter()) {
172        assert_eq!(actual, &Some(expected));
173    }
174
175    let errors = nodes
176        .iter()
177        .flat_map(|node| node.errors())
178        .map(|error| error.code)
179        .collect::<Vec<_>>();
180
181    assert_eq!(errors, vec![]);
182
183    NodesTester::new(nodes)
184}
185
186pub fn assert_parse_with_errors(code: &str, expected_errors: &[ErrorCode]) -> NodesTester {
187    let nodes = test_parse(code);
188    let actual = nodes
189        .iter()
190        .flat_map(|node| node.errors())
191        .map(|error| error.code)
192        .collect::<Vec<_>>();
193
194    assert_eq!(actual.len(), expected_errors.len());
195
196    for (actual, expected) in actual.iter().zip(expected_errors.iter()) {
197        assert_eq!(actual, &Some(expected));
198    }
199
200    NodesTester::new(nodes)
201}
202
203#[extension(trait SyntaxNodeExt)]
204impl SyntaxNode {
205    #[track_caller]
206    fn test_assert(&self, kind: SyntaxKind, text: &str) {
207        assert_eq!(
208            self.kind(),
209            kind,
210            "expected: {:?}, got: {:?}",
211            kind,
212            self.kind()
213        );
214        assert_eq!(
215            self.text(),
216            text,
217            "expected: {:?}, got: {:?}",
218            text,
219            self.text()
220        );
221    }
222
223    #[track_caller]
224    fn test_children(&self, kind: SyntaxKind) -> NodesTester {
225        assert_eq!(self.kind(), kind);
226
227        let children = self.children().cloned().collect::<Vec<_>>();
228        NodesTester::new(children)
229    }
230}
231
232pub struct NodesTester {
233    path: Vec<SyntaxKind>,
234    pub nodes: Vec<SyntaxNode>,
235    pos: usize,
236}
237
238impl NodesTester {
239    pub fn new(nodes: Vec<SyntaxNode>) -> Self {
240        Self {
241            nodes,
242            pos: 0,
243            path: vec![],
244        }
245    }
246
247    pub fn with_path(mut self, path: Vec<SyntaxKind>) -> Self {
248        self.path = path;
249        self
250    }
251
252    #[track_caller]
253    pub fn assert_next(&mut self, kind: SyntaxKind, text: &str) -> &mut Self {
254        let node = self.nodes.get(self.pos).or_else(|| panic!("No more nodes at {:?}. Expected: {kind:?}", self.path)).cloned().unwrap();
255
256        assert_eq!(
257            node.kind(),
258            kind,
259            "expected: {:?}, got: {:?} at {:?} ({node:?})",
260            kind,
261            node.kind(),
262            self.path,
263        );
264        assert_eq!(
265            node.text(),
266            text,
267            "expected: {:?}, got: {:?} at {:?} ({node:?})",
268            text,
269            node.text(),
270            self.path
271        );
272
273        self.pos += 1;
274
275        self
276    }
277
278
279    #[track_caller]
280    pub fn assert_next_warning(&mut self, warning: ErrorCode) -> &mut Self {
281        let node = self.nodes.get(self.pos).or_else(|| panic!("No more nodes at {:?}. Expected warning: {warning:?}", self.path)).cloned().unwrap();
282
283        assert_eq!(
284            node.kind(),
285            SyntaxKind::Error,
286            "Expected an error, got {:?} at {:?}",
287            node.kind(),
288            self.path
289        );
290
291        let warnings = node.warnings();
292        assert_eq!(
293            warnings.len(),
294            1,
295            "Expected an error, got {} warnings at {:?}",
296            warnings.len(),
297            self.path
298        );
299        assert_eq!(
300            warnings[0].code,
301            Some(&warning),
302            "Expected an error with code {:?}, got {:?} at {:?}",
303            warning,
304            warnings[0].code,
305            self.path
306        );
307
308        self.pos += 1;
309
310        self
311    }
312
313    pub fn move_to_end(&mut self) {
314        self.pos = self.nodes.len();
315    }
316
317    #[track_caller]
318    pub fn assert_next_error(&mut self, error: ErrorCode) -> &mut Self {
319        let node = self.nodes.get(self.pos).or_else(|| panic!("No more nodes at {:?}. Expected error: {error:?}", self.path)).cloned().unwrap();
320
321        assert_eq!(
322            node.kind(),
323            SyntaxKind::Error,
324            "Expected an error, got {:?} at {:?}",
325            node.kind(),
326            self.path
327        );
328
329        let errors = node.errors();
330        assert_eq!(
331            errors.len(),
332            1,
333            "Expected an error, got {} errors at {:?}",
334            errors.len(),
335            self.path
336        );
337        assert_eq!(
338            errors[0].code,
339            Some(&error),
340            "Expected an error with code {:?}, got {:?} at {:?}",
341            error,
342            errors[0].code,
343            self.path
344        );
345
346        self.pos += 1;
347
348        self
349    }
350
351    #[track_caller]
352    pub fn assert_next_children(
353        &mut self,
354        kind: SyntaxKind,
355        test_children: impl FnOnce(&mut Self),
356    ) -> &mut Self {
357        let node = self.nodes.get(self.pos).or_else(|| panic!("No more nodes at {:?}. Expected: {kind:?}", self.path)).cloned().unwrap();
358
359        assert_eq!(
360            node.kind(),
361            kind,
362            "expected: {:?}, got: {:?} at {:?} ({})",
363            kind,
364            node.kind(),
365            self.path,
366            node.to_text(),
367        );
368
369        let children = node.children().cloned().collect::<Vec<_>>();
370        let mut tester = NodesTester::new(children)
371            .with_path(self.path.iter().copied().chain(vec![kind]).collect());
372
373        test_children(&mut tester);
374        self.pos += 1;
375
376        self
377    }
378
379    #[track_caller]
380    pub fn assert_end(&self) {
381        assert_eq!(
382            self.pos,
383            self.nodes.len(),
384            "Not all nodes were consumed. at {:?}. Remaining: {:#?}",
385            self.path,
386            &self.nodes[self.pos..]
387        );
388    }
389}
390
391
392/// Macro to assert the structure and content of the parsed syntax tree from source code.
393///
394/// # Purpose
395///
396/// `assert_parse_tree!` allows you to declaratively specify the expected abstract syntax tree (AST)
397/// shape and tokens produced by your parser for a given source code snippet. It recursively
398/// matches nodes by their kinds (`SyntaxKind`) and token text, verifying both the hierarchical
399/// structure and token lexemes precisely.
400///
401/// This macro is primarily useful in parser/unit tests where you want to:
402/// - Assert nodes and their nested children are present in expected order.
403/// - Assert leaf tokens have expected text content.
404/// - Assert presence of parser errors and warnings at specific nodes.
405///
406///
407/// # Syntax Overview
408///
409/// ```ignore
410/// assert_parse_tree!(
411///     "source code string",
412///     RootNodeKind [            // Node with children
413///         ChildNodeKind1 ( "token_text" )  // Leaf node with exact text
414///         ChildNodeKind2 [                  // Nested node with children
415///             ...
416///         ]
417///         Warn(WARNING_CODE)               // Warning node with an error code enum or const
418///         Error(ERROR_CODE)                // Error node with an error code enum or const
419///         ...                             // Ellipsis to skip remaining children in this subtree
420///     ]
421/// );
422/// ```
423///
424/// - **Root node**: The first argument is the source code string to parse.
425/// - **Nodes with children**: Use `KindName [ ... ]` where `KindName` is a variant of your `SyntaxKind` enum.
426/// - **Leaf tokens**: Use `KindName("token_text")` where the string is the exact matched source token text.
427/// - **Warnings and errors**: Use `Warn(WARNING_CODE)` and `Error(ERROR_CODE)` where the code is a known error/warning enum or constant.
428/// - **Ellipsis (`...`)**: Use to ignore remaining children in a node subtree (useful to reduce test verbosity).
429///
430///
431/// # Notes
432///
433/// - `KindName` must be a variant of your `SyntaxKind` enum (without the `SyntaxKind::` prefix).
434/// - Leaf nodes *must* specify the exact token text as a string literal.
435/// - `Warn` and `Error` are reserved special nodes recognized by the macro for diagnostics.
436/// - The macro performs strict ordering checks: children must appear in exact order as specified.
437///
438///
439/// # Example: Simple function call
440///
441/// ```rust
442/// compose_syntax::test_utils::assert_parse_tree!(
443///     "f(a, b: c)",
444///     FuncCall [
445///         Ident("f")
446///         Args [
447///             LeftParen("(")
448///             Ident("a")
449///             Comma(",")
450///             Named [ Ident("b") Colon(":") Ident("c") ]
451///             RightParen(")")
452///         ]
453///     ]
454/// );
455/// ```
456///
457/// # Example: Error recovery with warnings and errors
458///
459/// ```rust
460/// compose_syntax::test_utils::assert_parse_tree!(
461///     r#"
462///     f(a, b, c
463///     1 + 2
464///     "#,
465///     FuncCall [
466///         Ident("f")
467///         Args [
468///             Error(compose_error_codes::E0001_UNCLOSED_DELIMITER)
469///             Ident("a")
470///             Comma(",")
471///             Ident("b")
472///             Comma(",")
473///             Ident("c")
474///             Error(compose_error_codes::E0009_ARGS_MISSING_COMMAS)
475///             Binary [
476///                 Int("1")
477///                 Plus("+")
478///                 Int("2")
479///             ]
480///         ]
481///     ]
482/// );
483/// ```
484///
485/// # Example: Skipping subtree content
486///
487/// ```rust
488/// compose_syntax::test_utils::assert_parse_tree!(
489///     "{ ref mut a => a }",
490///     Lambda [
491///         LeftBrace("{")
492///         Params [
493///             Param [
494///                 Ref("ref")
495///                 Mut("mut")
496///                 Ident("a")
497///             ]
498///         ]
499///         ...
500///     ]
501/// );
502/// ```
503///
504/// # Common mistakes
505///
506/// - Forgetting to provide the exact token text string for leaf nodes results in a compile error.
507/// - Using `Warn` or `Error` as `SyntaxKind` variants inside children nodes causes errors; they must use the special macro arms.
508/// # See also
509/// - [`SyntaxKind`] enum variants — the node/kind names used in this macro.
510/// - Parser error codes for `Warn` and `Error` nodes.
511/// - [`NodesTester`] struct for underlying test traversal and assertions.
512#[macro_export]
513macro_rules! assert_parse_tree {
514    // Entry point
515    ($src:expr, $($tree:tt)+) => {{
516        let nodes = $crate::test_utils::test_parse($src);
517        let mut p = $crate::test_utils::NodesTester::new(nodes);
518        $crate::test_utils::assert_parse_tree!(@seq p, $($tree)+);
519    }};
520
521    // Warning node
522    (@seq $parser:ident, Warn ( $code:expr ) $($rest:tt)*) => {
523        $parser.assert_next_warning($code);
524        $crate::test_utils::assert_parse_tree!(@seq $parser, $($rest)*);
525    };
526
527    // Error node
528    (@seq $parser:ident, Error ( $code:expr ) $($rest:tt)*) => {
529        $parser.assert_next_error($code);
530        $crate::test_utils::assert_parse_tree!(@seq $parser, $($rest)*);
531    };
532
533    // Node with children
534    (@seq $parser:ident, $kind:ident [ $($children:tt)+ ] $($rest:tt)*) => {
535        $parser.assert_next_children($crate::SyntaxKind::$kind, |p| {
536            $crate::test_utils::assert_parse_tree!(@seq p, $($children)+);
537        });
538        $crate::test_utils::assert_parse_tree!(@seq $parser, $($rest)*);
539    };
540
541
542    // Leaf node with string text
543    (@seq $parser:ident, $kind:ident ( $text:expr ) $($rest:tt)*) => {
544        $parser.assert_next($crate::SyntaxKind::$kind, $text);
545        $crate::test_utils::assert_parse_tree!(@seq $parser, $($rest)*);
546    };
547
548    // Invalid usage: leaf node without text — catch this as a helpful error
549    (@seq $parser:ident, $kind:ident $($rest:tt)*) => {
550        compile_error!(concat!("Leaf node `", stringify!($kind), "` must provide a string literal as text, like `", stringify!($kind), "(\"...\")`."))
551    };
552
553    // Deliberately empty children
554    (@seq $parser:ident, ...) => {
555        // ignore children
556        $parser.move_to_end();
557    };
558
559    // End
560    (@seq $parser:ident,) => {
561        $parser.assert_end();
562    };
563}
564
565pub use assert_parse_tree;
566
567
568#[extension(pub trait LexerAssert)]
569impl<'a> Lexer<'a> {
570    fn assert_next(&mut self, kind: SyntaxKind, text: &str, range: Range<usize>) -> &mut Lexer<'a> {
571        assert_eq!(
572            self.next(),
573            (
574                kind,
575                SyntaxNode::leaf(kind, text, Span::new(self.file_id, range))
576            )
577        );
578        self
579    }
580
581    fn assert_next_error(
582        &mut self,
583        kind: SyntaxKind,
584        message: &str,
585        text: &str,
586        range: Range<usize>,
587    ) -> &mut Lexer<'a> {
588        assert_eq!(
589            self.next(),
590            (
591                kind,
592                SyntaxNode::error(
593                    SyntaxError::new(message, Span::new(self.file_id, range)),
594                    text
595                )
596            )
597        );
598        self
599    }
600
601    fn assert_end(&mut self, index: usize) -> &mut Lexer<'a>  {
602        assert_eq!(
603            self.next(),
604            (
605                SyntaxKind::End,
606                SyntaxNode::leaf(SyntaxKind::End, "", Span::new(self.file_id, index..index))
607            )
608        );
609        self
610    }
611}
612/// Asserts that the token stream produced by the lexer exactly matches a sequence of expected tokens.
613///
614/// This macro is designed for testing your lexer. It verifies that each token is correctly
615/// identified and assigned the right text and span (`Range`), and that errors are reported as expected.
616///
617/// # Usage
618///
619/// The macro takes the source code as the first argument, followed by a list of expected tokens.
620/// Each token must be specified in the format:
621///
622/// ```ignore
623/// TokenKind("text", start..end)
624/// ```
625///
626/// Error tokens use a special syntax:
627///
628/// ```ignore
629/// !Error("message", "source", range)
630/// ```
631///
632/// # Examples
633///
634/// Basic token matching:
635///
636/// ```rust
637/// # use compose_syntax::assert_tokens;
638///
639/// assert_tokens!(
640///     "1.method()",
641///     Int("1", 0..1)
642///     Dot(".", 1..2)
643///     Ident("method", 2..8)
644///     LeftParen("(", 8..9)
645///     RightParen(")", 9..10)
646/// );
647/// ```
648///
649/// Unterminated string with error:
650///
651/// ```rust
652/// # use compose_syntax::assert_tokens;
653///
654/// assert_tokens!(
655///     "\"abc",
656///     !Error("unclosed string", "\"abc", 0..4)
657/// );
658/// ```
659///
660/// Float literal with exponent:
661///
662/// ```rust
663/// # use compose_syntax::assert_tokens;
664///
665/// assert_tokens!("1.0e1", Float("1.0e1", 0..5));
666/// assert_tokens!("1.0e-1", Float("1.0e-1", 0..6));
667/// ```
668///
669/// # Error Handling
670///
671/// If a token is incorrectly specified (e.g. an error is missing fields), a helpful
672/// compile-time error is emitted to guide correct macro usage:
673///
674/// ```ignore
675/// assert_tokens!(
676///     "abc",
677///     !Error("some msg") // ❌ Compile-time error: missing text and range
678/// );
679/// ```
680///
681/// # Implementation Notes
682///
683/// Internally, this macro expands into a recursive sequence of assertions on a `Lexer`.
684/// It ensures that the entire token stream is consumed, and provides detailed failure messages
685/// if any token mismatches occur.
686///
687/// # See Also
688///
689/// - [`SyntaxKind`] — The enum used to represent token types
690/// - [`assert_parse_tree`] — For testing parser output instead of lexer output
691#[macro_export]
692macro_rules! assert_tokens {
693    // Entry point
694    ($src:expr, $($tokens:tt)+) => {{
695        use $crate::test_utils::LexerAssert;
696        let file_id = $crate::test_utils::test_file_id();
697        let mut lexer = $crate::Lexer::new($src, file_id);
698        $crate::assert_tokens!(@seq lexer, $($tokens)+);
699    }};
700
701    // error
702    (@seq $lexer:ident, !Error ( $message:expr, $text:expr, $range:expr ) $($rest:tt)*) => {
703        $lexer.assert_next_error($crate::SyntaxKind::Error, $message, $text, $range);
704        $crate::assert_tokens!(@seq $lexer, $($rest)*);
705    };
706
707    // Leaf token
708    (@seq $lexer:ident, !Error ( $($tt:tt)* ) $($rest:tt)*) => {
709        compile_error!(concat!("Leaf node `Error` must provide a message, source text and a range, like `Error(\"message\", \"source text\", range)`."))
710    };
711
712    // Leaf token
713    (@seq $lexer:ident, $kind:ident ( $text:expr, $range:expr ) $($rest:tt)*) => {
714        $lexer.assert_next($crate::SyntaxKind::$kind, $text, $range);
715        $crate::assert_tokens!(@seq $lexer, $($rest)*);
716    };
717
718    // Incomplete token
719    (@seq $lexer:ident, $kind:ident $($rest:tt)*) => (
720        $crate::SyntaxKind::$kind;
721        compile_error!(concat!("Incomplete token `", stringify!($kind), "` must provide a text and a range, like `", stringify!($kind), "(\"text\", range)`."))
722    );
723
724    // End
725    (@seq $lexer:ident,) => {
726        $lexer.assert_end($lexer.cursor());
727    };
728}
729
730pub use assert_tokens;