// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #![doc = include_str!("README.md")] #![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")] extern crate proc_macro; use core::str::FromStr; use proc_macro::{Delimiter, TokenStream, TokenTree}; fn error(e: &str) -> String { format!("::core::compile_error!{{\"{e}\"}}") } fn generate_test(fn_name: &str, doc: &str, extra_args: usize) -> String { if fn_name.is_empty() { return error("Could not parse function name"); } if doc.is_empty() { return error("doc comments not found"); } let mut kind = None; let idx = match doc.find("```test\n") { Some(idx) => idx + 8, None => match doc.find("```test,") { None => return error("test not found"), Some(idx) => match doc[idx..].find('\n') { None => return error("test not found"), Some(newline) => { kind = Some(&doc[idx + 8..idx + newline]); idx + newline + 1 } }, }, }; let doc = &doc[(idx)..]; let idx = match doc.find("```\n") { Some(idx) => idx, None => return error("end of test not found"), }; let doc = &doc[..idx]; let verify = match kind { None => String::new(), Some(kind) => { format!( "syntax_nodes::{kind}::verify(SyntaxNode {{ node: rowan::SyntaxNode::new_root(p.builder.finish()), source_file: Default::default(), }});", ) } }; let mut tests = String::new(); for (i, line) in doc.split('\n').enumerate() { let line = line.trim(); if line.is_empty() { continue; } let follow_args = ", Default::default()".repeat(extra_args); tests += &format!( r#" #[test] fn parser_test_{fn_name}_{i}() {{ let mut diag = Default::default(); let mut p = DefaultParser::new("{line}", &mut diag); {fn_name}(&mut p{follow_args}); let has_error = p.diags.has_errors(); //#[cfg(feature = "display-diagnostics")] //p.diags.print(); assert!(!has_error); assert_eq!(p.cursor, p.tokens.len()); {verify} }} "#, ) } tests } #[proc_macro_attribute] pub fn parser_test(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut result = item.clone(); // The original function let mut doc = String::new(); let mut item = item.into_iter(); let mut fn_name = String::new(); let mut extra_args = 0; // Extract the doc comment. // Bail out once we find a token that does not fit the doc comment pattern loop { match item.next() { Some(TokenTree::Punct(p)) => { if p.as_char() != '#' { break; } } Some(TokenTree::Ident(i)) => { if i.to_string() == "fn" { fn_name = item.next().map_or_else(String::default, |i| i.to_string()); } break; } _ => break, } if let Some(TokenTree::Group(g)) = item.next() { if g.delimiter() != proc_macro::Delimiter::Bracket { break; } let mut attr = g.stream().into_iter(); if let Some(TokenTree::Ident(i)) = attr.next() { if i.to_string() != "doc" { break; } } else { break; } if let Some(TokenTree::Punct(p)) = attr.next() { if p.as_char() != '=' { break; } } else { break; } if let Some(TokenTree::Literal(lit)) = attr.next() { let s = lit.to_string(); // trim the quotes doc += &s[1..(s.len() - 1)]; doc += "\n"; } else { break; } } else { break; } } if fn_name.is_empty() { while let Some(tt) = item.next() { if tt.to_string() == "fn" { fn_name = item.next().map_or_else(String::default, |i| i.to_string()); break; } } } loop { match item.next() { None => break, Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => { let mut had_coma = false; for tt in g.stream().into_iter() { match tt { TokenTree::Punct(p) if p.as_char() == ',' => { had_coma = true; } TokenTree::Punct(p) if p.as_char() == ':' && had_coma => { extra_args += 1; had_coma = false; } _ => {} } } } _ => (), } } let test_function = TokenStream::from_str(&generate_test(&fn_name, &doc, extra_args)) .unwrap_or_else(|e| { TokenStream::from_str(&error(&format!("Lex error in generated test: {e:?}"))).unwrap() }); result.extend(test_function); result }