use proc_macro::TokenStream; use quote::{ToTokens, quote}; use sha2::{Digest, Sha256}; use syn::{ Error, Expr, ExprArray, Ident, Token, parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, }; struct MacroInvocation { rule: Ident, expressions: Punctuated, } impl Parse for MacroInvocation { fn parse(input: ParseStream) -> syn::Result { const RULE_VALUE: &str = "rule"; const EXPRESSSIONS_VALUE: &str = "expressions"; let rule_attribute = input.parse::()?; if rule_attribute != RULE_VALUE { return Err(Error::new( rule_attribute.span(), "expected `{RULE_VALUE:?}`", )); } input.parse::()?; let rule = input.parse::()?; input.parse::()?; let expressions = input.parse::()?; if expressions != EXPRESSSIONS_VALUE { return Err(Error::new( expressions.span(), "expected `{EXPRESSSIONS_VALUE:?}`", )); } input.parse::()?; let ExprArray { elems: expressions, .. } = input.parse::()?; input.parse::()?; Ok(MacroInvocation { rule, expressions }) } } pub fn generate_tests(input: TokenStream) -> TokenStream { let MacroInvocation { rule, expressions } = parse_macro_input!(input as MacroInvocation); expressions .into_iter() .map(|nix_expression| { let lint_test = make_test(&rule, TestKind::Lint, &nix_expression); let fix_test = make_test(&rule, TestKind::Fix, &nix_expression); quote! { #lint_test #fix_test } }) .collect::() .into() } #[derive(Clone, Copy, Debug)] enum TestKind { Lint, Fix, } fn make_test(rule: &Ident, kind: TestKind, nix_expression: &Expr) -> proc_macro2::TokenStream { let expression_hash = Sha256::digest(nix_expression.to_token_stream().to_string()); let expression_hash = hex::encode(expression_hash); let kind_str = match kind { TestKind::Lint => "lint", TestKind::Fix => "fix", }; let test_name = format!("{rule}_{kind_str}_{expression_hash}"); let test_ident = Ident::new(&test_name, nix_expression.span()); let snap_name = format!("{kind_str}_{expression_hash}"); let args = match kind { TestKind::Lint => quote! {&["check"]}, TestKind::Fix => quote! {&["fix", "--dry-run"]}, }; quote! { #[test] fn #test_ident() { let expression = #nix_expression; let stdout = _utils::test_cli(expression, #args).unwrap(); insta::assert_snapshot!(#snap_name, stdout, &format!("{expression:?}")); } } }