use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Item, ItemEnum, ItemMod, ItemStruct, parse_quote};
fn is_adze_attr(attr: &Attribute, name: &str) -> bool {
let segs: Vec<_> = attr.path().segments.iter().collect();
segs.len() == 2 && segs[0].ident == "adze" && segs[1].ident == name
}
fn adze_attr_names(attrs: &[Attribute]) -> Vec<String> {
attrs
.iter()
.filter_map(|a| {
let segs: Vec<_> = a.path().segments.iter().collect();
if segs.len() == 2 && segs[0].ident == "adze" {
Some(segs[1].ident.to_string())
} else {
None
}
})
.collect()
}
fn _parse_struct(tokens: TokenStream) -> ItemStruct {
syn::parse2(tokens).expect("failed to parse struct")
}
fn _parse_enum(tokens: TokenStream) -> ItemEnum {
syn::parse2(tokens).expect("failed to parse enum")
}
fn parse_mod(tokens: TokenStream) -> ItemMod {
syn::parse2(tokens).expect("failed to parse module")
}
fn module_items(m: &ItemMod) -> &[Item] {
&m.content.as_ref().expect("module has no content").1
}
fn find_language_type(m: &ItemMod) -> Option<String> {
module_items(m).iter().find_map(|item| match item {
Item::Enum(e) if e.attrs.iter().any(|a| is_adze_attr(a, "language")) => {
Some(e.ident.to_string())
}
Item::Struct(s) if s.attrs.iter().any(|a| is_adze_attr(a, "language")) => {
Some(s.ident.to_string())
}
_ => None,
})
}
fn has_grammar_attr(m: &ItemMod) -> bool {
m.attrs.iter().any(|a| is_adze_attr(a, "grammar"))
}
fn extract_grammar_name(m: &ItemMod) -> Option<String> {
m.attrs.iter().find_map(|a| {
if !is_adze_attr(a, "grammar") {
return None;
}
let expr: syn::Expr = a.parse_args().ok()?;
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = expr
{
Some(s.value())
} else {
None
}
})
}
fn count_items_by_type(m: &ItemMod, check: fn(&Item) -> bool) -> usize {
module_items(m).iter().filter(|i| check(i)).count()
}
#[test]
fn grammar_with_empty_name() {
let m = parse_mod(quote! {
#[adze::grammar("")]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] String),
}
}
});
assert!(has_grammar_attr(&m));
assert_eq!(extract_grammar_name(&m), Some(String::from("")));
}
#[test]
fn grammar_with_special_chars_in_name() {
let m = parse_mod(quote! {
#[adze::grammar("test_grammar-2024@v1")]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] String),
}
}
});
assert!(has_grammar_attr(&m));
assert_eq!(
extract_grammar_name(&m),
Some("test_grammar-2024@v1".to_string())
);
}
#[test]
fn grammar_with_whitespace_name() {
let m = parse_mod(quote! {
#[adze::grammar(" ")]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] String),
}
}
});
assert!(has_grammar_attr(&m));
assert_eq!(extract_grammar_name(&m), Some(" ".to_string()));
}
#[test]
fn grammar_with_very_long_name() {
let long_name = "a".repeat(100);
let m = parse_mod(quote! {
#[adze::grammar(#long_name)]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] String),
}
}
});
assert!(has_grammar_attr(&m));
let name = extract_grammar_name(&m);
assert!(name.is_some());
assert_eq!(name.unwrap().len(), 100);
}
#[test]
fn grammar_with_unicode_name() {
let m = parse_mod(quote! {
#[adze::grammar("gramma_文法")]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] String),
}
}
});
assert!(has_grammar_attr(&m));
assert_eq!(extract_grammar_name(&m), Some("gramma_文法".to_string()));
}
#[test]
fn language_attribute_on_struct() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub struct Root {
#[adze::leaf(pattern = r"\w+")]
name: String,
}
}
});
assert_eq!(find_language_type(&m), Some("Root".to_string()));
}
#[test]
fn language_attribute_on_enum() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] String),
}
}
});
assert_eq!(find_language_type(&m), Some("Expr".to_string()));
}
#[test]
fn leaf_with_single_char_pattern() {
let s: ItemStruct = parse_quote! {
pub struct Token {
#[adze::leaf(pattern = r"x")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_with_complex_regex() {
let s: ItemStruct = parse_quote! {
pub struct Token {
#[adze::leaf(pattern = r"(?:[0-9]+\.[0-9]*|\.[0-9]+)([eE][-+]?[0-9]+)?")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_with_unicode_pattern() {
let s: ItemStruct = parse_quote! {
pub struct Token {
#[adze::leaf(pattern = r"[\p{Letter}][\p{Letter}\p{Number}]*")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_with_empty_pattern() {
let s: ItemStruct = parse_quote! {
pub struct Token {
#[adze::leaf(pattern = r"")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_with_special_regex_chars() {
let s: ItemStruct = parse_quote! {
pub struct Token {
#[adze::leaf(pattern = r"(\[\]|{|}|\(|\))")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_with_special_text_literal() {
let e: ItemEnum = parse_quote! {
pub enum Op {
#[adze::leaf(text = "=>")]
Arrow,
#[adze::leaf(text = "::")]
DoubleColon,
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "leaf")) })
);
}
#[test]
fn leaf_with_quote_in_text() {
let e: ItemEnum = parse_quote! {
pub enum Quote {
#[adze::leaf(text = "\"")]
DoubleQuote,
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "leaf")) })
);
}
#[test]
fn precedence_positive_value() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec(42)]
Add(Box<Expr>, #[adze::leaf(text = "+")] (), Box<Expr>),
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "prec")) })
);
}
#[test]
fn precedence_negative_value() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec(-5)]
LowOp(Box<Expr>, #[adze::leaf(text = "|")] (), Box<Expr>),
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "prec")) })
);
}
#[test]
fn precedence_zero_value() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec(0)]
Op(Box<Expr>, #[adze::leaf(text = "~")] (), Box<Expr>),
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "prec")) })
);
}
#[test]
fn prec_left_multiple_levels() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec_left(1)]
Sub(Box<Expr>, #[adze::leaf(text = "-")] (), Box<Expr>),
#[adze::prec_left(2)]
Mul(Box<Expr>, #[adze::leaf(text = "*")] (), Box<Expr>),
}
};
let prec_left_count = e
.variants
.iter()
.filter(|v| v.attrs.iter().any(|a| is_adze_attr(a, "prec_left")))
.count();
assert_eq!(prec_left_count, 2);
}
#[test]
fn prec_right_multiple_levels() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec_right(1)]
Cons(Box<Expr>, #[adze::leaf(text = "::")] (), Box<Expr>),
#[adze::prec_right(3)]
Power(Box<Expr>, #[adze::leaf(text = "^")] (), Box<Expr>),
}
};
let prec_right_count = e
.variants
.iter()
.filter(|v| v.attrs.iter().any(|a| is_adze_attr(a, "prec_right")))
.count();
assert_eq!(prec_right_count, 2);
}
#[test]
fn multiple_attributes_on_variant() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec_left(1)]
Add(
Box<Expr>,
#[adze::leaf(text = "+")]
(),
Box<Expr>,
),
}
};
let add_variant = e.variants.iter().find(|v| v.ident == "Add").unwrap();
let attrs = adze_attr_names(&add_variant.attrs);
assert!(attrs.contains(&"prec_left".to_string()));
}
#[test]
fn skip_attribute_on_field() {
let s: ItemStruct = parse_quote! {
pub struct MyNode {
#[adze::leaf(pattern = r"\w+")]
name: String,
#[adze::skip(false)]
visited: bool,
#[adze::skip(true)]
processed: bool,
}
};
let skip_count = s
.fields
.iter()
.filter(|f| f.attrs.iter().any(|a| is_adze_attr(a, "skip")))
.count();
assert_eq!(skip_count, 2);
}
#[test]
fn extra_attribute_on_struct() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub enum Token {
Number(#[adze::leaf(pattern = r"\d+")] i32),
}
#[adze::extra]
struct Whitespace {
#[adze::leaf(pattern = r"\s")]
_ws: (),
}
}
});
let has_extra = module_items(&m).iter().any(|item| match item {
Item::Struct(s) => s.attrs.iter().any(|a| is_adze_attr(a, "extra")),
_ => false,
});
assert!(has_extra);
}
#[test]
fn unit_struct_with_leaf() {
let e: ItemEnum = parse_quote! {
pub enum Keyword {
#[adze::leaf(text = "if")]
If,
#[adze::leaf(text = "else")]
Else,
}
};
assert_eq!(e.variants.len(), 2);
assert!(
e.variants
.iter()
.all(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "leaf")) })
);
}
#[test]
fn tuple_struct_with_leaf() {
let s: ItemStruct = parse_quote! {
pub struct Number(
#[adze::leaf(pattern = r"\d+")]
i32
);
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn unit_enum_variant_with_leaf() {
let e: ItemEnum = parse_quote! {
pub enum Op {
#[adze::leaf(text = "+")]
Plus,
#[adze::leaf(text = "-")]
Minus,
}
};
assert_eq!(e.variants.len(), 2);
assert!(
e.variants
.iter()
.all(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "leaf")) })
);
}
#[test]
fn delimited_vector_field() {
let s: ItemStruct = parse_quote! {
pub struct List {
#[adze::delimited(
#[adze::leaf(text = ",")]
()
)]
items: Vec<Item>,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "delimited")));
}
#[test]
fn repeat_non_empty_field() {
let s: ItemStruct = parse_quote! {
pub struct List {
#[adze::repeat(non_empty = true)]
items: Vec<Item>,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "repeat")));
}
#[test]
fn complex_expression_grammar() {
let m = parse_mod(quote! {
#[adze::grammar("complex_expr")]
mod grammar {
#[adze::language]
pub enum Expr {
Literal(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec_left(1)]
Add(
Box<Expr>,
#[adze::leaf(text = "+")]
(),
Box<Expr>,
),
#[adze::prec_left(2)]
Mul(
Box<Expr>,
#[adze::leaf(text = "*")]
(),
Box<Expr>,
),
#[adze::prec_right(3)]
Power(
Box<Expr>,
#[adze::leaf(text = "^")]
(),
Box<Expr>,
),
}
}
});
assert!(has_grammar_attr(&m));
assert_eq!(find_language_type(&m), Some("Expr".to_string()));
}
#[test]
fn grammar_with_word_attribute() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub struct Code {
ident: Identifier,
}
#[adze::word]
pub struct Identifier {
#[adze::leaf(pattern = r"[a-zA-Z_]\w*")]
name: String,
}
}
});
let has_word = module_items(&m).iter().any(|item| match item {
Item::Struct(s) => s.attrs.iter().any(|a| is_adze_attr(a, "word")),
_ => false,
});
assert!(has_word);
}
#[test]
fn grammar_with_external_attribute() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub struct Code {
indent: IndentToken,
}
#[adze::external]
struct IndentToken {
#[adze::leaf(pattern = r"\t+")]
_indent: (),
}
}
});
let has_external = module_items(&m).iter().any(|item| match item {
Item::Struct(s) => s.attrs.iter().any(|a| is_adze_attr(a, "external")),
_ => false,
});
assert!(has_external);
}
#[test]
fn grammar_with_multiple_extras() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub struct Code {
#[adze::leaf(pattern = r"\w+")]
token: String,
}
#[adze::extra]
struct Whitespace {
#[adze::leaf(pattern = r" ")]
_ws: (),
}
#[adze::extra]
struct Newline {
#[adze::leaf(pattern = r"\n")]
_nl: (),
}
#[adze::extra]
struct Comment {
#[adze::leaf(pattern = r"//[^\n]*")]
_comment: (),
}
}
});
let extra_count = count_items_by_type(&m, |item| match item {
Item::Struct(s) => s.attrs.iter().any(|a| is_adze_attr(a, "extra")),
_ => false,
});
assert_eq!(extra_count, 3);
}
#[test]
fn optional_field_in_struct() {
let s: ItemStruct = parse_quote! {
pub struct MaybeNum {
#[adze::leaf(pattern = r"\d+")]
value: Option<i32>,
}
};
assert_eq!(s.fields.len(), 1);
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn vector_field_in_struct() {
let s: ItemStruct = parse_quote! {
pub struct NumList {
#[adze::repeat(non_empty = false)]
items: Vec<Item>,
}
};
assert_eq!(s.fields.len(), 1);
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "repeat")));
}
#[test]
fn boxed_recursive_type() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
Neg(
#[adze::leaf(text = "-")]
(),
Box<Expr>,
),
}
};
assert_eq!(e.variants.len(), 2);
}
#[test]
fn leaf_with_simple_transform() {
let s: ItemStruct = parse_quote! {
pub struct Num {
#[adze::leaf(pattern = r"\d+", transform = |s| s.len())]
digit_count: usize,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_with_complex_transform() {
let s: ItemStruct = parse_quote! {
pub struct Num {
#[adze::leaf(pattern = r"\d+", transform = |s: &str| s.parse::<i32>().unwrap_or(0))]
value: i32,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn public_visibility_language() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
}
}
});
assert_eq!(find_language_type(&m), Some("Expr".to_string()));
}
#[test]
fn private_visibility_language() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
}
}
});
assert_eq!(find_language_type(&m), Some("Expr".to_string()));
}
#[test]
fn pub_crate_visibility() {
let m = parse_mod(quote! {
#[adze::grammar("test")]
mod grammar {
#[adze::language]
pub(crate) struct Root {
#[adze::leaf(pattern = r"\w+")]
name: String,
}
}
});
assert_eq!(find_language_type(&m), Some("Root".to_string()));
}
#[test]
fn large_precedence_value() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec(999999)]
Op(Box<Expr>, #[adze::leaf(text = "+")] (), Box<Expr>),
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "prec")) })
);
}
#[test]
fn large_negative_precedence() {
let e: ItemEnum = parse_quote! {
pub enum Expr {
Number(#[adze::leaf(pattern = r"\d+")] i32),
#[adze::prec(-999999)]
Op(Box<Expr>, #[adze::leaf(text = "|")] (), Box<Expr>),
}
};
assert!(
e.variants
.iter()
.any(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "prec")) })
);
}
#[test]
fn leaf_pattern_with_escapes() {
let s: ItemStruct = parse_quote! {
pub struct Token {
#[adze::leaf(pattern = r"\d+\.\d+")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_pattern_with_alternation() {
let e: ItemEnum = parse_quote! {
pub enum Token {
#[adze::leaf(pattern = r"true|false")]
Bool(String),
#[adze::leaf(pattern = r"null|nil|undefined")]
Null(String),
}
};
assert_eq!(e.variants.len(), 2);
assert!(
e.variants
.iter()
.all(|v| { v.attrs.iter().any(|a| is_adze_attr(a, "leaf")) })
);
}
#[test]
fn leaf_pattern_with_char_class() {
let s: ItemStruct = parse_quote! {
pub struct HexToken {
#[adze::leaf(pattern = r"[0-9a-fA-F]+")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}
#[test]
fn leaf_pattern_with_negated_class() {
let s: ItemStruct = parse_quote! {
pub struct LineToken {
#[adze::leaf(pattern = r"[^\n]+")]
value: String,
}
};
let field = s.fields.iter().next().unwrap();
assert!(field.attrs.iter().any(|a| is_adze_attr(a, "leaf")));
}