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}