Skip to main content

subtensor_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::ToTokens;
4use syn::{Error, ItemStruct, LitStr, Result, parse2, visit_mut::visit_item_struct_mut};
5
6mod call_filter_group;
7mod visitor;
8use visitor::*;
9
10/// Freezes the layout of a struct to the current hash of its fields, ensuring that future
11/// changes require updating the hash.
12///
13/// ```
14/// use subtensor_macros::freeze_struct;
15///
16/// #[freeze_struct("13f75e4ea46b4e80")]
17/// #[derive(Copy, Clone, PartialEq, Eq)]
18/// pub struct MyStruct {
19///     pub a: u32,
20///     pub b: u64,
21/// }
22/// ```
23#[proc_macro_attribute]
24pub fn freeze_struct(attr: TokenStream, tokens: TokenStream) -> TokenStream {
25    match freeze_struct_impl(attr, tokens) {
26        Ok(item_struct) => item_struct.to_token_stream().into(),
27        Err(err) => err.to_compile_error().into(),
28    }
29}
30
31/// Defines a reusable runtime call filter group and derives its metadata from the same allowlist.
32///
33/// Example: `call_filter_group!(GroupName, [RuntimeCall::Foobar(pallet_foobar::Call::some_call)]);`
34#[proc_macro]
35pub fn call_filter_group(input: TokenStream) -> TokenStream {
36    let parsed = syn::parse_macro_input!(input as call_filter_group::CallFilterGroupInput);
37    match call_filter_group::generate(parsed) {
38        Ok(tokens) => tokens.into(),
39        Err(err) => err.to_compile_error().into(),
40    }
41}
42
43fn freeze_struct_impl(
44    attr: impl Into<TokenStream2>,
45    tokens: impl Into<TokenStream2>,
46) -> Result<ItemStruct> {
47    let attr = attr.into();
48    let tokens = tokens.into();
49
50    let item = parse2::<ItemStruct>(tokens)?;
51    let mut item_clone = item.clone();
52
53    let mut visitor = CleanDocComments::new();
54    visit_item_struct_mut(&mut visitor, &mut item_clone);
55
56    let calculated_hash = generate_hash(&item_clone);
57    let calculated_hash_hex = format!("{calculated_hash:x}");
58
59    if attr.is_empty() {
60        return Err(Error::new_spanned(
61            item,
62            format!(
63                "You must provide a hashcode in the `freeze_struct` attribute to freeze this struct.\n\n\
64            expected hashcode: `#[freeze_struct(\"{calculated_hash_hex}\")]`"
65            ),
66        ));
67    }
68
69    let parsed_attr = parse2::<LitStr>(attr)?;
70    let provided_hash_hex = parsed_attr.value().to_lowercase();
71
72    if provided_hash_hex != calculated_hash_hex {
73        return Err(Error::new_spanned(
74            item,
75            format!(
76                "You have made a non-trivial change to this struct and the provided hashcode no longer matches:\n{provided_hash_hex} != {calculated_hash_hex}\n\n\
77                If this was intentional, please update the hashcode in the `freeze_struct` attribute to:\n\
78                {calculated_hash_hex}\n\nNote that if you are changing a storage struct in any way, including simply re-ordering fields, \
79                you will need a migration to prevent data corruption."
80            ),
81        ));
82    }
83    Ok(item)
84}