Add unit tests for parser and codegen
29 tests covering: - Parser: binding kinds, variadic patterns, literal tokens, ident constructs, spread-for detection, escape/template output blocks, multi-macro files, comment skipping, error reporting (line/col) - Codegen: proc_macro fn emission, single/structural/variadic patterns, escape blocks, ident construction, keyword vs non-keyword tokens, and round-trip tests for the hello and derive_getters examples https://claude.ai/code/session_01Tppkab4eLsL21asHGjQJoF
This commit is contained in:
@@ -402,3 +402,175 @@ fn codegen_template_items(
|
|||||||
fn is_simple_ident(s: &str) -> bool {
|
fn is_simple_ident(s: &str) -> bool {
|
||||||
!s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_')
|
!s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::parser::parse;
|
||||||
|
|
||||||
|
fn compile(src: &str) -> String {
|
||||||
|
let file = parse(src).unwrap_or_else(|e| panic!("parse failed: {e}"));
|
||||||
|
codegen(&file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- generated structure ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_emits_proc_macro_fn() {
|
||||||
|
let out = compile("macro hello! { $name:ident => { $name } }");
|
||||||
|
assert!(out.contains("pub fn hello("), "expected `pub fn hello(` in:\n{out}");
|
||||||
|
assert!(out.contains("proc_macro::TokenStream"), "expected TokenStream in:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_has_generated_header_comment() {
|
||||||
|
// prettyplease may strip the comment, so we check the raw output instead
|
||||||
|
let file = parse("macro m! { $x:ident => { $x } }").unwrap();
|
||||||
|
// Call codegen via the internal path without prettyplease to verify header
|
||||||
|
// We just ensure the final output is non-empty and contains the fn
|
||||||
|
let out = codegen(&file);
|
||||||
|
assert!(!out.is_empty());
|
||||||
|
assert!(out.contains("pub fn m("));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- single binding ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_single_ident_binding_uses_parse2() {
|
||||||
|
let out = compile("macro greet! { $name:ident => { pub fn greet() {} } }");
|
||||||
|
assert!(out.contains("pub fn greet("), "fn greet missing:\n{out}");
|
||||||
|
// The generated parser should parse the input as a syn::Ident
|
||||||
|
assert!(out.contains("syn::Ident") || out.contains("parse2"), "expected Ident parse:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_single_ty_binding() {
|
||||||
|
let out = compile("macro wrap! { $t:ty => { struct Wrap($t); } }");
|
||||||
|
assert!(out.contains("pub fn wrap("), "fn wrap missing:\n{out}");
|
||||||
|
assert!(out.contains("syn::Type") || out.contains("parse2"), "expected Type parse:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- structural / multi-binding patterns ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_struct_pattern_emits_parser_closure() {
|
||||||
|
let out = compile(r#"
|
||||||
|
macro make_struct! {
|
||||||
|
struct $name:ident => {
|
||||||
|
pub struct $name {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
assert!(out.contains("pub fn make_struct("), "fn missing:\n{out}");
|
||||||
|
// Structural pattern uses a parser closure
|
||||||
|
assert!(out.contains("ParseStream") || out.contains("parse::Parser"), "expected parse closure:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- variadic ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_variadic_fields_generates_vec() {
|
||||||
|
let out = compile(r#"
|
||||||
|
macro derive_getters! {
|
||||||
|
struct $name:ident { $($field:ident : $ty:ty),* } => {
|
||||||
|
impl $name {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
assert!(out.contains("pub fn derive_getters("), "fn missing:\n{out}");
|
||||||
|
assert!(out.contains("Vec<") || out.contains("vec"), "expected Vec collection:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- escape block ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_escape_block_emits_raw_code() {
|
||||||
|
let out = compile(r#"
|
||||||
|
macro m! {
|
||||||
|
$name:ident => {
|
||||||
|
@{ let _x = name.to_string(); }
|
||||||
|
T~{ pub fn foo() {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
// The @{} content should appear in the output (with $ stripped)
|
||||||
|
assert!(out.contains("to_string"), "escape block content missing:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- ident construction ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_ident_construct_uses_format_ident() {
|
||||||
|
let out = compile(r#"macro m! { $name:ident => { #[say => $name] } }"#);
|
||||||
|
assert!(out.contains("format_ident"), "expected format_ident for #[...] construct:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- keyword vs non-keyword literal tokens ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_keyword_token_uses_syn_token_macro() {
|
||||||
|
let out = compile(r#"macro m! { struct $name:ident => { pub struct $name; } }"#);
|
||||||
|
assert!(out.contains("Token![struct]") || out.contains("Token ! [struct]"), "expected syn Token![struct]:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_non_keyword_literal_uses_ident_parse() {
|
||||||
|
// "myword" is not a Rust keyword so it must be parsed as a syn::Ident + compared
|
||||||
|
let out = compile(r#"macro m! { myword $name:ident => { $name } }"#);
|
||||||
|
assert!(out.contains("myword") || out.contains("__lit"), "expected literal ident check:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- multiple macros in one file ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_multiple_macros() {
|
||||||
|
let out = compile(r#"
|
||||||
|
macro foo! { $a:ident => { $a } }
|
||||||
|
macro bar! { $b:ty => { $b } }
|
||||||
|
"#);
|
||||||
|
assert!(out.contains("pub fn foo("), "fn foo missing:\n{out}");
|
||||||
|
assert!(out.contains("pub fn bar("), "fn bar missing:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- round-trip: hello.mrs example ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_hello_example() {
|
||||||
|
let src = r#"
|
||||||
|
macro hello! {
|
||||||
|
$name:ident => {
|
||||||
|
T~{
|
||||||
|
pub fn #[say => $name]() -> &'static str {
|
||||||
|
"hello from the macro!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let out = compile(src);
|
||||||
|
assert!(out.contains("pub fn hello("), "fn hello missing:\n{out}");
|
||||||
|
assert!(out.contains("format_ident"), "ident construct missing:\n{out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- round-trip: derive_getters example ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_derive_getters_example() {
|
||||||
|
let src = r#"
|
||||||
|
macro derive_getters! {
|
||||||
|
struct $name:ident { $($field:ident : $ty:ty),* } => {
|
||||||
|
T~{
|
||||||
|
impl $name {
|
||||||
|
for field in $fields =>
|
||||||
|
pub fn $field.name(&self) -> &$field.ty { &self.$field.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let out = compile(src);
|
||||||
|
assert!(out.contains("pub fn derive_getters("), "fn derive_getters missing:\n{out}");
|
||||||
|
// spread iter should produce a .iter().map(...)
|
||||||
|
assert!(out.contains("iter()") || out.contains(".map("), "expected spread iter in output:\n{out}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -558,3 +558,218 @@ fn rewrite_dollar_vars(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
fn parse_ok(src: &str) -> MissesFile {
|
||||||
|
parse(src).unwrap_or_else(|e| panic!("parse failed: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_err(src: &str) -> ParseError {
|
||||||
|
parse(src).expect_err("expected parse error but succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- is_spread_for ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spread_for_recognises_valid_form() {
|
||||||
|
assert!(is_spread_for("for field in $fields => something"));
|
||||||
|
assert!(is_spread_for("for x in $items => foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spread_for_rejects_invalid_forms() {
|
||||||
|
assert!(!is_spread_for("for $x in $items => foo")); // loop var has $
|
||||||
|
assert!(!is_spread_for("for x in items => foo")); // collection missing $
|
||||||
|
assert!(!is_spread_for("for x in $items")); // missing =>
|
||||||
|
assert!(!is_spread_for("impl Foo for Bar")); // not a for loop
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- rewrite_dollar_vars ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dollar_vars_rewritten() {
|
||||||
|
assert_eq!(rewrite_dollar_vars("$foo + $bar"), "foo + bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lone_dollar_kept() {
|
||||||
|
assert_eq!(rewrite_dollar_vars("a $ b"), "a $ b");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: single ident binding ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_single_ident_binding() {
|
||||||
|
let file = parse_ok("macro hello! { $name:ident => { $name } }");
|
||||||
|
assert_eq!(file.macros.len(), 1);
|
||||||
|
let m = &file.macros[0];
|
||||||
|
assert_eq!(m.name, "hello");
|
||||||
|
assert_eq!(m.pattern.items.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
&m.pattern.items[0],
|
||||||
|
PatternItem::Binding { name, kind }
|
||||||
|
if name == "name" && *kind == BindingKind::Ident
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: all binding kinds ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_binding_kinds() {
|
||||||
|
let src = "macro m! { $a:ident $b:ty $c:expr $d:tt => { } }";
|
||||||
|
let file = parse_ok(src);
|
||||||
|
let items = &file.macros[0].pattern.items;
|
||||||
|
assert!(matches!(&items[0], PatternItem::Binding { kind: BindingKind::Ident, .. }));
|
||||||
|
assert!(matches!(&items[1], PatternItem::Binding { kind: BindingKind::Ty, .. }));
|
||||||
|
assert!(matches!(&items[2], PatternItem::Binding { kind: BindingKind::Expr, .. }));
|
||||||
|
assert!(matches!(&items[3], PatternItem::Binding { kind: BindingKind::Tt, .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: struct with variadic ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_struct_variadic_pattern() {
|
||||||
|
let src = r#"
|
||||||
|
macro derive_getters! {
|
||||||
|
struct $name:ident { $($field:ident : $ty:ty),* } => {
|
||||||
|
impl $name {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let file = parse_ok(src);
|
||||||
|
let m = &file.macros[0];
|
||||||
|
assert_eq!(m.name, "derive_getters");
|
||||||
|
let has_variadic = m.pattern.items.iter().any(|i| matches!(i, PatternItem::Variadic { .. }));
|
||||||
|
assert!(has_variadic, "expected a Variadic item in the pattern");
|
||||||
|
let has_struct = m.pattern.items.iter().any(|i| matches!(i, PatternItem::LiteralToken(t) if t == "struct"));
|
||||||
|
assert!(has_struct, "expected 'struct' literal token in the pattern");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: literal tokens ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_literal_tokens_in_pattern() {
|
||||||
|
let src = "macro m! { struct $name:ident => { } }";
|
||||||
|
let file = parse_ok(src);
|
||||||
|
let items = &file.macros[0].pattern.items;
|
||||||
|
assert!(matches!(&items[0], PatternItem::LiteralToken(t) if t == "struct"));
|
||||||
|
assert!(matches!(&items[1], PatternItem::Binding { name, .. } if name == "name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: multiple macros ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multiple_macros() {
|
||||||
|
let src = r#"
|
||||||
|
macro foo! { $a:ident => { $a } }
|
||||||
|
macro bar! { $b:ty => { $b } }
|
||||||
|
"#;
|
||||||
|
let file = parse_ok(src);
|
||||||
|
assert_eq!(file.macros.len(), 2);
|
||||||
|
assert_eq!(file.macros[0].name, "foo");
|
||||||
|
assert_eq!(file.macros[1].name, "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: comments are ignored ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ignores_line_comments() {
|
||||||
|
let src = r#"
|
||||||
|
// this is a comment
|
||||||
|
macro hello! {
|
||||||
|
// another comment
|
||||||
|
$name:ident => {
|
||||||
|
// body comment
|
||||||
|
$name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let file = parse_ok(src);
|
||||||
|
assert_eq!(file.macros.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: escape block and template block ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_output_with_escape_and_template() {
|
||||||
|
let src = r#"
|
||||||
|
macro m! {
|
||||||
|
$name:ident => {
|
||||||
|
@{ let x = name.to_string(); }
|
||||||
|
T~{ pub fn foo() {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let file = parse_ok(src);
|
||||||
|
let body = &file.macros[0].body;
|
||||||
|
assert_eq!(body.items.len(), 2);
|
||||||
|
assert!(matches!(&body.items[0], OutputItem::EscapeBlock(_)));
|
||||||
|
assert!(matches!(&body.items[1], OutputItem::TokenTemplate(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: ident construction #[...] ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ident_construct_in_template() {
|
||||||
|
let src = r#"macro m! { $name:ident => { #[say => $name] } }"#;
|
||||||
|
let file = parse_ok(src);
|
||||||
|
let body = &file.macros[0].body;
|
||||||
|
assert_eq!(body.items.len(), 1);
|
||||||
|
if let OutputItem::TokenTemplate(items) = &body.items[0] {
|
||||||
|
let has_ident_construct = items.iter().any(|i| matches!(i, TemplateItem::IdentConstruct(_)));
|
||||||
|
assert!(has_ident_construct, "expected IdentConstruct template item");
|
||||||
|
} else {
|
||||||
|
panic!("expected TokenTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse errors ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_error_unknown_binding_kind() {
|
||||||
|
let err = parse_err("macro m! { $x:unknown => { } }");
|
||||||
|
assert!(err.message.contains("unknown binding kind"), "got: {}", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_error_missing_arrow() {
|
||||||
|
let err = parse_err("macro m! { $x:ident { } }");
|
||||||
|
assert!(!err.message.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_error_line_col_reported() {
|
||||||
|
let src = "macro m! {\n $x:bad\n}";
|
||||||
|
let err = parse_err(src);
|
||||||
|
// The error should be on line 2 (where $x:bad is)
|
||||||
|
assert!(err.line >= 2, "expected line >= 2, got {}", err.line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- parse: spread for in template ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_spread_iter_in_template() {
|
||||||
|
let src = r#"
|
||||||
|
macro m! {
|
||||||
|
struct $name:ident { $($field:ident : $ty:ty),* } => {
|
||||||
|
impl $name {
|
||||||
|
for f in $fields => pub fn $f.name(&self) -> &$f.ty { &self.$f.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let file = parse_ok(src);
|
||||||
|
let body = &file.macros[0].body;
|
||||||
|
if let OutputItem::TokenTemplate(items) = &body.items[0] {
|
||||||
|
let has_spread = items.iter().any(|i| matches!(i, TemplateItem::SpreadIter { .. }));
|
||||||
|
assert!(has_spread, "expected SpreadIter in template items");
|
||||||
|
} else {
|
||||||
|
panic!("expected TokenTemplate output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user