Lints and suggestions for the Nix programming language
at main 103 lines 3.0 kB view raw
1use proc_macro::TokenStream; 2use quote::{ToTokens, quote}; 3use sha2::{Digest, Sha256}; 4use syn::{ 5 Error, Expr, ExprArray, Ident, Token, 6 parse::{Parse, ParseStream}, 7 parse_macro_input, 8 punctuated::Punctuated, 9 spanned::Spanned, 10 token::Comma, 11}; 12 13struct MacroInvocation { 14 rule: Ident, 15 expressions: Punctuated<Expr, Comma>, 16} 17 18impl Parse for MacroInvocation { 19 fn parse(input: ParseStream) -> syn::Result<Self> { 20 const RULE_VALUE: &str = "rule"; 21 const EXPRESSSIONS_VALUE: &str = "expressions"; 22 let rule_attribute = input.parse::<Ident>()?; 23 24 if rule_attribute != RULE_VALUE { 25 return Err(Error::new( 26 rule_attribute.span(), 27 "expected `{RULE_VALUE:?}`", 28 )); 29 } 30 31 input.parse::<Token![:]>()?; 32 let rule = input.parse::<Ident>()?; 33 input.parse::<Token![,]>()?; 34 let expressions = input.parse::<Ident>()?; 35 36 if expressions != EXPRESSSIONS_VALUE { 37 return Err(Error::new( 38 expressions.span(), 39 "expected `{EXPRESSSIONS_VALUE:?}`", 40 )); 41 } 42 43 input.parse::<Token![:]>()?; 44 let ExprArray { 45 elems: expressions, .. 46 } = input.parse::<ExprArray>()?; 47 48 input.parse::<Token![,]>()?; 49 Ok(MacroInvocation { rule, expressions }) 50 } 51} 52 53pub fn generate_tests(input: TokenStream) -> TokenStream { 54 let MacroInvocation { rule, expressions } = parse_macro_input!(input as MacroInvocation); 55 expressions 56 .into_iter() 57 .map(|nix_expression| { 58 let lint_test = make_test(&rule, TestKind::Lint, &nix_expression); 59 let fix_test = make_test(&rule, TestKind::Fix, &nix_expression); 60 61 quote! { 62 #lint_test 63 64 #fix_test 65 } 66 }) 67 .collect::<proc_macro2::TokenStream>() 68 .into() 69} 70 71#[derive(Clone, Copy, Debug)] 72enum TestKind { 73 Lint, 74 Fix, 75} 76 77fn make_test(rule: &Ident, kind: TestKind, nix_expression: &Expr) -> proc_macro2::TokenStream { 78 let expression_hash = Sha256::digest(nix_expression.to_token_stream().to_string()); 79 let expression_hash = hex::encode(expression_hash); 80 81 let kind_str = match kind { 82 TestKind::Lint => "lint", 83 TestKind::Fix => "fix", 84 }; 85 86 let test_name = format!("{rule}_{kind_str}_{expression_hash}"); 87 let test_ident = Ident::new(&test_name, nix_expression.span()); 88 let snap_name = format!("{kind_str}_{expression_hash}"); 89 90 let args = match kind { 91 TestKind::Lint => quote! {&["check"]}, 92 TestKind::Fix => quote! {&["fix", "--dry-run"]}, 93 }; 94 95 quote! { 96 #[test] 97 fn #test_ident() { 98 let expression = #nix_expression; 99 let stdout = _utils::test_cli(expression, #args).unwrap(); 100 insta::assert_snapshot!(#snap_name, stdout, &format!("{expression:?}")); 101 } 102 } 103}