Lints and suggestions for the Nix programming language
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}