Skip to main content

adze_macro/
lib.rs

1// Proc-macro crate is safe code only
2#![forbid(unsafe_code)]
3#![cfg_attr(feature = "strict_docs", deny(missing_docs))]
4#![cfg_attr(not(feature = "strict_docs"), allow(missing_docs))]
5
6//! Procedural macros for adze grammar definition
7
8use quote::ToTokens;
9use syn::{ItemMod, parse_macro_input};
10
11mod errors;
12mod expansion;
13use expansion::*;
14
15#[proc_macro_attribute]
16/// Marks the top-level AST node where parsing should start.
17///
18/// Exactly one type inside an [`macro@grammar`] module must carry this attribute.
19/// It can be applied to either a `struct` or an `enum`.
20///
21/// ## Examples
22///
23/// As a struct (single production):
24/// ```ignore
25/// #[adze::language]
26/// pub struct Program {
27///     statements: Vec<Statement>,
28/// }
29/// ```
30///
31/// As an enum (multiple alternatives):
32/// ```ignore
33/// #[adze::language]
34/// pub enum Expr {
35///     Number(
36///         #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
37///         i32
38///     ),
39///     #[adze::prec_left(1)]
40///     Add(Box<Expr>, #[adze::leaf(text = "+")] (), Box<Expr>),
41/// }
42/// ```
43pub fn language(
44    _attr: proc_macro::TokenStream,
45    item: proc_macro::TokenStream,
46) -> proc_macro::TokenStream {
47    item
48}
49
50#[proc_macro_attribute]
51/// This annotation marks a node as extra, which can safely be skipped while parsing.
52/// This is useful for handling whitespace/newlines/comments.
53///
54/// ## Example
55/// ```ignore
56/// #[adze::extra]
57/// struct Whitespace {
58///     #[adze::leaf(pattern = r"\s")]
59///     _whitespace: (),
60/// }
61/// ```
62pub fn extra(
63    _attr: proc_macro::TokenStream,
64    item: proc_macro::TokenStream,
65) -> proc_macro::TokenStream {
66    item
67}
68
69#[proc_macro_attribute]
70/// Defines a field which matches a specific token in the source string.
71/// The token can be defined by passing one of two arguments
72/// - `text`: a string literal that will be exactly matched
73/// - `pattern`: a regular expression that will be matched against the source string
74///
75/// If the resulting token needs to be converted into a richer type at runtime,
76/// such as a number, then the `transform` argument can be used to specify a function
77/// that will be called with the token's text.
78///
79/// The attribute can also be applied to a struct or enum variant with no fields.
80///
81/// ## Examples
82///
83/// Using the `leaf` attribute on a field:
84/// ```ignore
85/// Number(
86///     #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
87///     u32
88/// )
89/// ```
90///
91/// Using the attribute on a unit struct or unit enum variant:
92/// ```ignore
93/// #[adze::leaf(text = "9")]
94/// struct BigDigit;
95///
96/// enum SmallDigit {
97///     #[adze::leaf(text = "0")]
98///     Zero,
99///     #[adze::leaf(text = "1")]
100///     One,
101/// }
102/// ```
103///
104pub fn leaf(
105    _attr: proc_macro::TokenStream,
106    item: proc_macro::TokenStream,
107) -> proc_macro::TokenStream {
108    item
109}
110
111#[proc_macro_attribute]
112/// Defines a field that does not correspond to anything in the input string,
113/// such as some metadata. Takes a single, unnamed argument, which is the value
114/// used to populate the field at runtime.
115///
116/// ## Example
117/// ```ignore
118/// struct MyNode {
119///    ...,
120///    #[adze::skip(false)]
121///    node_visited: bool
122/// }
123/// ```
124pub fn skip(
125    _attr: proc_macro::TokenStream,
126    item: proc_macro::TokenStream,
127) -> proc_macro::TokenStream {
128    item
129}
130
131#[proc_macro_attribute]
132/// Defines a precedence level for a non-terminal that has no associativity.
133///
134/// This annotation takes a single, unnamed parameter, which specifies the precedence level.
135/// This is used to resolve conflicts with other non-terminals, so that the one with the higher
136/// precedence will bind more tightly (appear lower in the parse tree).
137///
138/// ## Example
139/// ```ignore
140/// #[adze::prec(1)]
141/// PriorityExpr(Box<Expr>, Box<Expr>)
142/// ```
143pub fn prec(
144    _attr: proc_macro::TokenStream,
145    item: proc_macro::TokenStream,
146) -> proc_macro::TokenStream {
147    item
148}
149
150#[proc_macro_attribute]
151/// Defines a precedence level for a non-terminal that should be left-associative.
152/// For example, with subtraction we expect 1 - 2 - 3 to be parsed as (1 - 2) - 3,
153/// which corresponds to a left-associativity.
154///
155/// This annotation takes a single, unnamed parameter, which specifies the precedence level.
156/// This is used to resolve conflicts with other non-terminals, so that the one with the higher
157/// precedence will bind more tightly (appear lower in the parse tree).
158///
159/// ## Example
160/// ```ignore
161/// #[adze::prec_left(1)]
162/// Subtract(Box<Expr>, Box<Expr>)
163/// ```
164pub fn prec_left(
165    _attr: proc_macro::TokenStream,
166    item: proc_macro::TokenStream,
167) -> proc_macro::TokenStream {
168    item
169}
170
171#[proc_macro_attribute]
172/// Defines a precedence level for a non-terminal that should be right-associative.
173/// For example, with cons we could have 1 :: 2 :: 3 to be parsed as 1 :: (2 :: 3),
174/// which corresponds to a right-associativity.
175///
176/// This annotation takes a single, unnamed parameter, which specifies the precedence level.
177/// This is used to resolve conflicts with other non-terminals, so that the one with the higher
178/// precedence will bind more tightly (appear lower in the parse tree).
179///
180/// ## Example
181/// ```ignore
182/// #[adze::prec_right(1)]
183/// Cons(Box<Expr>, Box<Expr>)
184/// ```
185pub fn prec_right(
186    _attr: proc_macro::TokenStream,
187    item: proc_macro::TokenStream,
188) -> proc_macro::TokenStream {
189    item
190}
191
192#[proc_macro_attribute]
193/// On `Vec<_>` typed fields, specifies a non-terminal that should be parsed in between the elements.
194/// The `#[adze::repeat]` annotation must be used on the field as well.
195///
196/// This annotation takes a single, unnamed argument, which specifies a field type to parse. This can
197/// either be a reference to another type, or can be defined as a `leaf` field. Generally, the argument
198/// is parsed using the same rules as an unnamed field of an enum variant.
199///
200/// ## Example
201/// ```ignore
202/// #[adze::delimited(
203///     #[adze::leaf(text = ",")]
204///     ()
205/// )]
206/// numbers: Vec<Number>
207/// ```
208pub fn delimited(
209    _attr: proc_macro::TokenStream,
210    item: proc_macro::TokenStream,
211) -> proc_macro::TokenStream {
212    item
213}
214
215#[proc_macro_attribute]
216/// On `Vec<_>` typed fields, specifies additional config for how the repeated elements should
217/// be parsed. In particular, this annotation takes the following named arguments:
218/// - `non_empty` - if this argument is `true`, then there must be at least one element parsed
219///
220/// ## Example
221/// ```ignore
222/// #[adze::repeat(non_empty = true)]
223/// numbers: Vec<Number>
224/// ```
225pub fn repeat(
226    _attr: proc_macro::TokenStream,
227    item: proc_macro::TokenStream,
228) -> proc_macro::TokenStream {
229    item
230}
231
232/// Marks a rule as an external scanner token. External scanners are implemented in separate
233/// code and handle context-sensitive tokens like indentation or heredocs.
234///
235/// ## Example
236/// ```ignore
237/// #[adze::external]
238/// struct IndentToken;
239/// ```
240#[proc_macro_attribute]
241pub fn external(
242    _attr: proc_macro::TokenStream,
243    item: proc_macro::TokenStream,
244) -> proc_macro::TokenStream {
245    item
246}
247
248/// Marks a token as the word token for the grammar. Word tokens are used to handle
249/// keywords vs identifiers disambiguation.
250///
251/// ## Example
252/// ```ignore
253/// #[adze::word]
254/// #[adze::leaf(pattern = r"[a-zA-Z_]\w*")]
255/// struct Identifier(String);
256/// ```
257#[proc_macro_attribute]
258pub fn word(
259    _attr: proc_macro::TokenStream,
260    item: proc_macro::TokenStream,
261) -> proc_macro::TokenStream {
262    item
263}
264
265/// Mark a module to be analyzed for an Adze grammar. Takes a single, unnamed argument, which
266/// specifies the name of the grammar. This name must be unique across all Adze grammars within
267/// a compilation unit.
268///
269/// The module must contain exactly one type annotated with [`macro@language`] to serve as the
270/// parse entry point. Other types in the module define the remaining grammar rules.
271///
272/// ## Example
273/// ```ignore
274/// #[adze::grammar("arithmetic")]
275/// mod grammar {
276///     #[adze::language]
277///     pub enum Expr {
278///         Number(
279///             #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
280///             i32
281///         ),
282///         #[adze::prec_left(1)]
283///         Add(
284///             Box<Expr>,
285///             #[adze::leaf(text = "+")]
286///             (),
287///             Box<Expr>,
288///         ),
289///     }
290///
291///     #[adze::extra]
292///     struct Whitespace {
293///         #[adze::leaf(pattern = r"\s")]
294///         _whitespace: (),
295///     }
296/// }
297/// ```
298#[proc_macro_attribute]
299pub fn grammar(
300    attr: proc_macro::TokenStream,
301    input: proc_macro::TokenStream,
302) -> proc_macro::TokenStream {
303    let attr_tokens: proc_macro2::TokenStream = attr.into();
304    let module: ItemMod = parse_macro_input!(input);
305    let expanded = expand_grammar(syn::parse_quote! {
306        #[adze::grammar[#attr_tokens]]
307        #module
308    })
309    .map(ToTokens::into_token_stream)
310    .unwrap_or_else(syn::Error::into_compile_error);
311    proc_macro::TokenStream::from(expanded)
312}
313
314#[cfg(test)]
315mod tests {
316    use std::fs::File;
317    use std::io::{Read, Write};
318    use std::process::Command;
319
320    use quote::ToTokens;
321    use syn::{Result, parse_quote};
322    use tempfile::tempdir;
323
324    use super::expand_grammar;
325
326    fn snapshot_name(base: &str) -> String {
327        if cfg!(feature = "pure-rust") {
328            format!("{base}__pure_rust")
329        } else {
330            base.to_owned()
331        }
332    }
333
334    macro_rules! assert_feature_snapshot {
335        ($name:literal, $expr:expr $(,)?) => {
336            insta::assert_snapshot!(snapshot_name($name), $expr);
337        };
338    }
339
340    fn rustfmt_code(code: &str) -> String {
341        let dir = tempdir().unwrap();
342        let file_path = dir.path().join("temp.rs");
343        let mut file = File::create(file_path.clone()).unwrap();
344
345        writeln!(file, "{code}").unwrap();
346        drop(file);
347
348        Command::new("rustfmt")
349            .arg(file_path.to_str().unwrap())
350            .spawn()
351            .unwrap()
352            .wait()
353            .unwrap();
354
355        let mut file = File::open(file_path).unwrap();
356        let mut data = String::new();
357        file.read_to_string(&mut data).unwrap();
358        drop(file);
359        dir.close().unwrap();
360        data
361    }
362
363    #[test]
364    fn enum_transformed_fields() -> Result<()> {
365        assert_feature_snapshot!("enum_transformed_fields", rustfmt_code(
366            &expand_grammar(parse_quote! {
367                #[adze::grammar("test")]
368                mod grammar {
369                    #[adze::language]
370                    pub enum Expression {
371                        Number(
372                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse::<i32>().unwrap())]
373                            i32
374                        ),
375                    }
376                }
377            })?
378            .to_token_stream()
379            .to_string()
380        ));
381
382        Ok(())
383    }
384
385    #[test]
386    fn enum_recursive() -> Result<()> {
387        assert_feature_snapshot!(
388            "enum_recursive",
389            rustfmt_code(
390                &expand_grammar(parse_quote! {
391                    #[adze::grammar("test")]
392                    mod grammar {
393                        #[adze::language]
394                        pub enum Expression {
395                            Number(
396                                #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
397                                i32
398                            ),
399                            Neg(
400                                #[adze::leaf(text = "-")]
401                                (),
402                                Box<Expression>
403                            ),
404                        }
405                    }
406                })?
407                .to_token_stream()
408                .to_string()
409            )
410        );
411
412        Ok(())
413    }
414
415    #[test]
416    fn enum_prec_left() -> Result<()> {
417        assert_feature_snapshot!(
418            "enum_prec_left",
419            rustfmt_code(
420                &expand_grammar(parse_quote! {
421                    #[adze::grammar("test")]
422                    mod grammar {
423                        #[adze::language]
424                        pub enum Expression {
425                            Number(
426                                #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
427                                i32
428                            ),
429                            #[adze::prec_left(1)]
430                            Sub(
431                                Box<Expression>,
432                                #[adze::leaf(text = "-")]
433                                (),
434                                Box<Expression>
435                            ),
436                        }
437                    }
438                })?
439                .to_token_stream()
440                .to_string()
441            )
442        );
443
444        Ok(())
445    }
446
447    #[test]
448    fn struct_extra() -> Result<()> {
449        assert_feature_snapshot!("struct_extra", rustfmt_code(
450            &expand_grammar(parse_quote! {
451                #[adze::grammar("test")]
452                mod grammar {
453                    #[adze::language]
454                    pub enum Expression {
455                        Number(
456                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())] i32,
457                        ),
458                    }
459
460                    #[adze::extra]
461                    struct Whitespace {
462                        #[adze::leaf(pattern = r"\s")]
463                        _whitespace: (),
464                    }
465                }
466            })?
467            .to_token_stream()
468            .to_string()
469        ));
470
471        Ok(())
472    }
473
474    #[test]
475    fn grammar_unboxed_field() -> Result<()> {
476        assert_feature_snapshot!("grammar_unboxed_field", rustfmt_code(
477            &expand_grammar(parse_quote! {
478                #[adze::grammar("test")]
479                mod grammar {
480                    #[adze::language]
481                    pub struct Language {
482                        e: Expression,
483                    }
484
485                    pub enum Expression {
486                        Number(
487                            #[adze::leaf(pattern = r"\d+", transform = |v: &str| v.parse::<i32>().unwrap())]
488                            i32
489                        ),
490                    }
491                }
492            })?
493            .to_token_stream()
494            .to_string()
495        ));
496
497        Ok(())
498    }
499
500    #[test]
501    fn struct_repeat() -> Result<()> {
502        assert_feature_snapshot!(
503            "struct_repeat",
504            rustfmt_code(
505                &expand_grammar(parse_quote! {
506                    #[adze::grammar("test")]
507                    mod grammar {
508                        #[adze::language]
509                        pub struct NumberList {
510                            numbers: Vec<Number>,
511                        }
512
513                        pub struct Number {
514                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
515                            v: i32
516                        }
517
518                        #[adze::extra]
519                        struct Whitespace {
520                            #[adze::leaf(pattern = r"\s")]
521                            _whitespace: (),
522                        }
523                    }
524                })?
525                .to_token_stream()
526                .to_string()
527            )
528        );
529
530        Ok(())
531    }
532
533    #[test]
534    fn struct_optional() -> Result<()> {
535        assert_feature_snapshot!(
536            "struct_optional",
537            rustfmt_code(
538                &expand_grammar(parse_quote! {
539                    #[adze::grammar("test")]
540                    mod grammar {
541                        #[adze::language]
542                        pub struct Language {
543                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
544                            v: Option<i32>,
545                            t: Option<Number>,
546                        }
547
548                        pub struct Number {
549                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
550                            v: i32
551                        }
552                    }
553                })?
554                .to_token_stream()
555                .to_string()
556            )
557        );
558
559        Ok(())
560    }
561
562    #[test]
563    fn enum_with_unamed_vector() -> Result<()> {
564        assert_feature_snapshot!(
565            "enum_with_unamed_vector",
566            rustfmt_code(
567                &expand_grammar(parse_quote! {
568                    #[adze::grammar("test")]
569                    mod grammar {
570                        pub struct Number {
571                                #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
572                                value: u32
573                        }
574
575                        #[adze::language]
576                        pub enum Expr {
577                            Numbers(
578                                #[adze::repeat(non_empty = true)]
579                                Vec<Number>
580                            )
581                        }
582                    }
583                })?
584                .to_token_stream()
585                .to_string()
586            )
587        );
588
589        Ok(())
590    }
591
592    #[test]
593    fn enum_with_named_field() -> Result<()> {
594        assert_feature_snapshot!("enum_with_named_field", rustfmt_code(
595            &expand_grammar(parse_quote! {
596                #[adze::grammar("test")]
597                mod grammar {
598                    #[adze::language]
599                    pub enum Expr {
600                        Number(
601                                #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
602                                u32
603                        ),
604                        Neg {
605                            #[adze::leaf(text = "!")]
606                            _bang: (),
607                            value: Box<Expr>,
608                        }
609                    }
610                }
611            })?
612            .to_token_stream()
613            .to_string()
614        ));
615
616        Ok(())
617    }
618
619    #[test]
620    fn spanned_in_vec() -> Result<()> {
621        assert_feature_snapshot!(
622            "spanned_in_vec",
623            rustfmt_code(
624                &expand_grammar(parse_quote! {
625                    #[adze::grammar("test")]
626                    mod grammar {
627                        use adze::Spanned;
628
629                        #[adze::language]
630                        pub struct NumberList {
631                            numbers: Vec<Spanned<Number>>,
632                        }
633
634                        pub struct Number {
635                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
636                            v: i32
637                        }
638
639                        #[adze::extra]
640                        struct Whitespace {
641                            #[adze::leaf(pattern = r"\s")]
642                            _whitespace: (),
643                        }
644                    }
645                })?
646                .to_token_stream()
647                .to_string()
648            )
649        );
650
651        Ok(())
652    }
653
654    // === Error case tests ===
655
656    #[test]
657    fn error_grammar_missing_name() {
658        let result = expand_grammar(parse_quote! {
659            #[adze::grammar]
660            mod grammar {
661                #[adze::language]
662                pub enum Expr {
663                    Number(#[adze::leaf(pattern = r"\d+")] String),
664                }
665            }
666        });
667        let err = result.unwrap_err();
668        assert!(
669            err.to_string().contains("grammar name"),
670            "Expected 'grammar name' error, got: {err}"
671        );
672    }
673
674    #[test]
675    fn error_grammar_non_string_name() {
676        let result = expand_grammar(parse_quote! {
677            #[adze::grammar(42)]
678            mod grammar {
679                #[adze::language]
680                pub enum Expr {
681                    Number(#[adze::leaf(pattern = r"\d+")] String),
682                }
683            }
684        });
685        let err = result.unwrap_err();
686        assert!(
687            err.to_string().contains("string literal"),
688            "Expected 'string literal' error, got: {err}"
689        );
690    }
691
692    #[test]
693    fn error_grammar_missing_language_attr() {
694        let result = expand_grammar(parse_quote! {
695            #[adze::grammar("test")]
696            mod grammar {
697                pub enum Expr {
698                    Number(#[adze::leaf(pattern = r"\d+")] String),
699                }
700            }
701        });
702        let err = result.unwrap_err();
703        assert!(
704            err.to_string().contains("adze::language"),
705            "Expected 'adze::language' error, got: {err}"
706        );
707    }
708
709    #[test]
710    fn error_grammar_on_non_module() {
711        // expand_grammar expects an ItemMod; using parse_quote with a module
712        // that has no body simulates the semicolon-only module case
713        let result = expand_grammar(parse_quote! {
714            #[adze::grammar("test")]
715            mod grammar;
716        });
717        let err = result.unwrap_err();
718        assert!(
719            err.to_string().contains("inline contents"),
720            "Expected 'inline contents' error, got: {err}"
721        );
722    }
723
724    // === Valid attribute variation tests ===
725
726    #[test]
727    fn enum_prec_right() -> Result<()> {
728        assert_feature_snapshot!(
729            "enum_prec_right",
730            rustfmt_code(
731                &expand_grammar(parse_quote! {
732                    #[adze::grammar("test")]
733                    mod grammar {
734                        #[adze::language]
735                        pub enum Expression {
736                            Number(
737                                #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
738                                i32
739                            ),
740                            #[adze::prec_right(1)]
741                            Cons(
742                                Box<Expression>,
743                                #[adze::leaf(text = "::")]
744                                (),
745                                Box<Expression>
746                            ),
747                        }
748                    }
749                })?
750                .to_token_stream()
751                .to_string()
752            )
753        );
754        Ok(())
755    }
756
757    #[test]
758    fn enum_prec_no_assoc() -> Result<()> {
759        assert_feature_snapshot!(
760            "enum_prec_no_assoc",
761            rustfmt_code(
762                &expand_grammar(parse_quote! {
763                    #[adze::grammar("test")]
764                    mod grammar {
765                        #[adze::language]
766                        pub enum Expression {
767                            Number(
768                                #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
769                                i32
770                            ),
771                            #[adze::prec(2)]
772                            Compare(
773                                Box<Expression>,
774                                #[adze::leaf(text = "==")]
775                                (),
776                                Box<Expression>
777                            ),
778                        }
779                    }
780                })?
781                .to_token_stream()
782                .to_string()
783            )
784        );
785        Ok(())
786    }
787
788    #[test]
789    fn struct_delimited_repeat() -> Result<()> {
790        assert_feature_snapshot!(
791            "struct_delimited_repeat",
792            rustfmt_code(
793                &expand_grammar(parse_quote! {
794                    #[adze::grammar("test")]
795                    mod grammar {
796                        #[adze::language]
797                        pub struct NumberList {
798                            #[adze::delimited(
799                                #[adze::leaf(text = ",")]
800                                ()
801                            )]
802                            numbers: Vec<Number>,
803                        }
804
805                        pub struct Number {
806                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
807                            v: i32
808                        }
809                    }
810                })?
811                .to_token_stream()
812                .to_string()
813            )
814        );
815        Ok(())
816    }
817
818    #[test]
819    fn struct_with_skip_field() -> Result<()> {
820        assert_feature_snapshot!(
821            "struct_with_skip_field",
822            rustfmt_code(
823                &expand_grammar(parse_quote! {
824                    #[adze::grammar("test")]
825                    mod grammar {
826                        #[adze::language]
827                        pub struct MyNode {
828                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
829                            value: i32,
830                            #[adze::skip(false)]
831                            visited: bool,
832                        }
833                    }
834                })?
835                .to_token_stream()
836                .to_string()
837            )
838        );
839        Ok(())
840    }
841
842    #[test]
843    fn struct_repeat_non_empty() -> Result<()> {
844        assert_feature_snapshot!(
845            "struct_repeat_non_empty",
846            rustfmt_code(
847                &expand_grammar(parse_quote! {
848                    #[adze::grammar("test")]
849                    mod grammar {
850                        #[adze::language]
851                        pub struct NumberList {
852                            #[adze::repeat(non_empty = true)]
853                            numbers: Vec<Number>,
854                        }
855
856                        pub struct Number {
857                            #[adze::leaf(pattern = r"\d+", transform = |v| v.parse().unwrap())]
858                            v: i32
859                        }
860                    }
861                })?
862                .to_token_stream()
863                .to_string()
864            )
865        );
866        Ok(())
867    }
868
869    #[test]
870    fn leaf_text_literal() -> Result<()> {
871        assert_feature_snapshot!(
872            "leaf_text_literal",
873            rustfmt_code(
874                &expand_grammar(parse_quote! {
875                    #[adze::grammar("test")]
876                    mod grammar {
877                        #[adze::language]
878                        pub enum Token {
879                            #[adze::leaf(text = "+")]
880                            Plus,
881                            #[adze::leaf(text = "-")]
882                            Minus,
883                        }
884                    }
885                })?
886                .to_token_stream()
887                .to_string()
888            )
889        );
890        Ok(())
891    }
892
893    #[test]
894    fn leaf_pattern_only() -> Result<()> {
895        assert_feature_snapshot!(
896            "leaf_pattern_only",
897            rustfmt_code(
898                &expand_grammar(parse_quote! {
899                    #[adze::grammar("test")]
900                    mod grammar {
901                        #[adze::language]
902                        pub struct Identifier {
903                            #[adze::leaf(pattern = r"[a-zA-Z_]\w*")]
904                            name: String,
905                        }
906                    }
907                })?
908                .to_token_stream()
909                .to_string()
910            )
911        );
912        Ok(())
913    }
914
915    #[test]
916    fn grammar_with_word_attr() -> Result<()> {
917        assert_feature_snapshot!(
918            "grammar_with_word_attr",
919            rustfmt_code(
920                &expand_grammar(parse_quote! {
921                    #[adze::grammar("test")]
922                    mod grammar {
923                        #[adze::language]
924                        pub struct Code {
925                            ident: Identifier,
926                        }
927
928                        #[adze::word]
929                        pub struct Identifier {
930                            #[adze::leaf(pattern = r"[a-zA-Z_]\w*")]
931                            name: String,
932                        }
933                    }
934                })?
935                .to_token_stream()
936                .to_string()
937            )
938        );
939        Ok(())
940    }
941
942    #[test]
943    fn grammar_with_external_attr() -> Result<()> {
944        assert_feature_snapshot!(
945            "grammar_with_external_attr",
946            rustfmt_code(
947                &expand_grammar(parse_quote! {
948                    #[adze::grammar("test")]
949                    mod grammar {
950                        #[adze::language]
951                        pub struct Code {
952                            #[adze::leaf(pattern = r"\w+")]
953                            token: String,
954                        }
955
956                        #[adze::external]
957                        struct IndentToken {
958                            #[adze::leaf(pattern = r"\t+")]
959                            _indent: (),
960                        }
961                    }
962                })?
963                .to_token_stream()
964                .to_string()
965            )
966        );
967        Ok(())
968    }
969
970    #[test]
971    fn enum_unit_variant_leaf() -> Result<()> {
972        // Unit variants with leaf attributes are a special code path
973        assert_feature_snapshot!(
974            "enum_unit_variant_leaf",
975            rustfmt_code(
976                &expand_grammar(parse_quote! {
977                    #[adze::grammar("test")]
978                    mod grammar {
979                        #[adze::language]
980                        pub enum Keyword {
981                            #[adze::leaf(text = "if")]
982                            If,
983                            #[adze::leaf(text = "else")]
984                            Else,
985                            #[adze::leaf(text = "while")]
986                            While,
987                        }
988                    }
989                })?
990                .to_token_stream()
991                .to_string()
992            )
993        );
994        Ok(())
995    }
996
997    #[test]
998    fn multiple_extra_types() -> Result<()> {
999        assert_feature_snapshot!(
1000            "multiple_extra_types",
1001            rustfmt_code(
1002                &expand_grammar(parse_quote! {
1003                    #[adze::grammar("test")]
1004                    mod grammar {
1005                        #[adze::language]
1006                        pub struct Code {
1007                            #[adze::leaf(pattern = r"\w+")]
1008                            token: String,
1009                        }
1010
1011                        #[adze::extra]
1012                        struct Whitespace {
1013                            #[adze::leaf(pattern = r"\s")]
1014                            _ws: (),
1015                        }
1016
1017                        #[adze::extra]
1018                        struct Comment {
1019                            #[adze::leaf(pattern = r"//[^\n]*")]
1020                            _comment: (),
1021                        }
1022                    }
1023                })?
1024                .to_token_stream()
1025                .to_string()
1026            )
1027        );
1028        Ok(())
1029    }
1030}