solana_frozen_abi_macro/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4
5// Define dummy macro_attribute and macro_derive for stable rustc
6
7#[cfg(not(feature = "frozen-abi"))]
8#[proc_macro_attribute]
9pub fn frozen_abi(_attrs: TokenStream, item: TokenStream) -> TokenStream {
10    item
11}
12
13#[cfg(not(feature = "frozen-abi"))]
14#[proc_macro_derive(AbiExample)]
15pub fn derive_abi_sample(_item: TokenStream) -> TokenStream {
16    "".parse().unwrap()
17}
18
19#[cfg(not(feature = "frozen-abi"))]
20#[proc_macro_derive(AbiEnumVisitor)]
21pub fn derive_abi_enum_visitor(_item: TokenStream) -> TokenStream {
22    "".parse().unwrap()
23}
24
25#[cfg(feature = "frozen-abi")]
26use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
27#[cfg(feature = "frozen-abi")]
28use quote::{quote, ToTokens};
29#[cfg(feature = "frozen-abi")]
30use syn::{
31    parse_macro_input, Attribute, Error, Fields, Ident, Item, ItemEnum, ItemStruct, ItemType,
32    LitStr, Variant,
33};
34
35#[cfg(feature = "frozen-abi")]
36fn filter_serde_attrs(attrs: &[Attribute]) -> bool {
37    fn contains_skip(tokens: TokenStream2) -> bool {
38        for token in tokens.into_iter() {
39            match token {
40                TokenTree::Group(group) => {
41                    if contains_skip(group.stream()) {
42                        return true;
43                    }
44                }
45                TokenTree::Ident(ident) => {
46                    if ident == "skip" {
47                        return true;
48                    }
49                }
50                TokenTree::Punct(_) | TokenTree::Literal(_) => (),
51            }
52        }
53
54        false
55    }
56
57    for attr in attrs {
58        if !attr.path().is_ident("serde") {
59            continue;
60        }
61
62        if contains_skip(attr.to_token_stream()) {
63            return true;
64        }
65    }
66
67    false
68}
69
70#[cfg(feature = "frozen-abi")]
71fn filter_allow_attrs(attrs: &mut Vec<Attribute>) {
72    attrs.retain(|attr| {
73        let ss = &attr.path().segments.first().unwrap().ident.to_string();
74        ss.starts_with("allow")
75    });
76}
77
78#[cfg(feature = "frozen-abi")]
79fn derive_abi_sample_enum_type(input: ItemEnum) -> TokenStream {
80    let type_name = &input.ident;
81
82    let mut sample_variant = quote! {};
83    let mut sample_variant_found = false;
84
85    for variant in &input.variants {
86        let variant_name = &variant.ident;
87        let variant = &variant.fields;
88        if *variant == Fields::Unit {
89            sample_variant.extend(quote! {
90                #type_name::#variant_name
91            });
92        } else if let Fields::Unnamed(variant_fields) = variant {
93            let mut fields = quote! {};
94            for field in &variant_fields.unnamed {
95                if !(field.ident.is_none() && field.colon_token.is_none()) {
96                    unimplemented!("tuple enum: {:?}", field);
97                }
98                let field_type = &field.ty;
99                fields.extend(quote! {
100                    <#field_type>::example(),
101                });
102            }
103            sample_variant.extend(quote! {
104                #type_name::#variant_name(#fields)
105            });
106        } else if let Fields::Named(variant_fields) = variant {
107            let mut fields = quote! {};
108            for field in &variant_fields.named {
109                if field.ident.is_none() || field.colon_token.is_none() {
110                    unimplemented!("tuple enum: {:?}", field);
111                }
112                let field_type = &field.ty;
113                let field_name = &field.ident;
114                fields.extend(quote! {
115                    #field_name: <#field_type>::example(),
116                });
117            }
118            sample_variant.extend(quote! {
119                #type_name::#variant_name{#fields}
120            });
121        } else {
122            unimplemented!("{:?}", variant);
123        }
124
125        if !sample_variant_found {
126            sample_variant_found = true;
127            break;
128        }
129    }
130
131    if !sample_variant_found {
132        unimplemented!("empty enum");
133    }
134
135    let mut attrs = input.attrs.clone();
136    filter_allow_attrs(&mut attrs);
137    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
138
139    let result = quote! {
140        #[automatically_derived]
141        #( #attrs )*
142        impl #impl_generics ::solana_frozen_abi::abi_example::AbiExample for #type_name #ty_generics #where_clause {
143            fn example() -> Self {
144                ::solana_frozen_abi::__private::log::info!(
145                    "AbiExample for enum: {}",
146                    std::any::type_name::<#type_name #ty_generics>()
147                );
148                #sample_variant
149            }
150        }
151    };
152    result.into()
153}
154
155#[cfg(feature = "frozen-abi")]
156fn derive_abi_sample_struct_type(input: ItemStruct) -> TokenStream {
157    let type_name = &input.ident;
158    let mut sample_fields = quote! {};
159    let fields = &input.fields;
160
161    match fields {
162        Fields::Named(_) => {
163            for field in fields {
164                let field_name = &field.ident;
165                sample_fields.extend(quote! {
166                    #field_name: AbiExample::example(),
167                });
168            }
169            sample_fields = quote! {
170                { #sample_fields }
171            }
172        }
173        Fields::Unnamed(_) => {
174            for _ in fields {
175                sample_fields.extend(quote! {
176                    AbiExample::example(),
177                });
178            }
179            sample_fields = quote! {
180                ( #sample_fields )
181            }
182        }
183        _ => unimplemented!("fields: {:?}", fields),
184    }
185
186    let mut attrs = input.attrs.clone();
187    filter_allow_attrs(&mut attrs);
188    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
189    let turbofish = ty_generics.as_turbofish();
190
191    let result = quote! {
192        #[automatically_derived]
193        #( #attrs )*
194        impl #impl_generics ::solana_frozen_abi::abi_example::AbiExample for #type_name #ty_generics #where_clause {
195            fn example() -> Self {
196                ::solana_frozen_abi::__private::log::info!(
197                    "AbiExample for struct: {}",
198                    std::any::type_name::<#type_name #ty_generics>()
199                );
200                use ::solana_frozen_abi::abi_example::AbiExample;
201
202                #type_name #turbofish #sample_fields
203            }
204        }
205    };
206
207    result.into()
208}
209
210#[cfg(feature = "frozen-abi")]
211#[proc_macro_derive(AbiExample)]
212pub fn derive_abi_sample(item: TokenStream) -> TokenStream {
213    let item = parse_macro_input!(item as Item);
214
215    match item {
216        Item::Struct(input) => derive_abi_sample_struct_type(input),
217        Item::Enum(input) => derive_abi_sample_enum_type(input),
218        _ => Error::new_spanned(item, "AbiSample isn't applicable; only for struct and enum")
219            .to_compile_error()
220            .into(),
221    }
222}
223
224#[cfg(feature = "frozen-abi")]
225fn do_derive_abi_enum_visitor(input: ItemEnum) -> TokenStream {
226    let type_name = &input.ident;
227    let mut serialized_variants = quote! {};
228    let mut variant_count: u64 = 0;
229    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
230    for variant in &input.variants {
231        // Don't digest a variant with serde(skip)
232        if filter_serde_attrs(&variant.attrs) {
233            continue;
234        };
235        let sample_variant = quote_sample_variant(type_name, &ty_generics, variant);
236        variant_count = if let Some(variant_count) = variant_count.checked_add(1) {
237            variant_count
238        } else {
239            break;
240        };
241        serialized_variants.extend(quote! {
242            #sample_variant;
243            Serialize::serialize(&sample_variant, digester.create_enum_child()?)?;
244        });
245    }
246
247    let type_str = format!("{type_name}");
248    (quote! {
249        impl #impl_generics ::solana_frozen_abi::abi_example::AbiEnumVisitor for #type_name #ty_generics #where_clause {
250            fn visit_for_abi(&self, digester: &mut ::solana_frozen_abi::abi_digester::AbiDigester) -> ::solana_frozen_abi::abi_digester::DigestResult {
251                let enum_name = #type_str;
252                use ::serde::ser::Serialize;
253                use ::solana_frozen_abi::abi_example::AbiExample;
254                digester.update_with_string(::std::format!("enum {} (variants = {})", enum_name, #variant_count));
255                #serialized_variants
256                digester.create_child()
257            }
258        }
259    }).into()
260}
261
262#[cfg(feature = "frozen-abi")]
263#[proc_macro_derive(AbiEnumVisitor)]
264pub fn derive_abi_enum_visitor(item: TokenStream) -> TokenStream {
265    let item = parse_macro_input!(item as Item);
266
267    match item {
268        Item::Enum(input) => do_derive_abi_enum_visitor(input),
269        _ => Error::new_spanned(item, "AbiEnumVisitor not applicable; only for enum")
270            .to_compile_error()
271            .into(),
272    }
273}
274
275#[cfg(feature = "frozen-abi")]
276fn quote_for_test(
277    test_mod_ident: &Ident,
278    type_name: &Ident,
279    expected_digest: &str,
280) -> TokenStream2 {
281    // escape from nits.sh...
282    let p = Ident::new(&("ep".to_owned() + "rintln"), Span::call_site());
283    quote! {
284        #[cfg(test)]
285        mod #test_mod_ident {
286            use super::*;
287            use ::solana_frozen_abi::abi_example::{AbiExample, AbiEnumVisitor};
288
289            #[test]
290            fn test_abi_digest() {
291                ::solana_logger::setup();
292                let mut digester = ::solana_frozen_abi::abi_digester::AbiDigester::create();
293                let example = <#type_name>::example();
294                let result = <_>::visit_for_abi(&&example, &mut digester);
295                let mut hash = digester.finalize();
296                // pretty-print error
297                if result.is_err() {
298                    ::solana_frozen_abi::__private::log::error!("digest error: {:#?}", result);
299                }
300                result.unwrap();
301                let actual_digest = ::std::format!("{}", hash);
302                if ::std::env::var("SOLANA_ABI_BULK_UPDATE").is_ok() {
303                    if #expected_digest != actual_digest {
304                        #p!("sed -i -e 's/{}/{}/g' $(git grep --files-with-matches frozen_abi)", #expected_digest, hash);
305                    }
306                    ::solana_frozen_abi::__private::log::warn!("Not testing the abi digest under SOLANA_ABI_BULK_UPDATE!");
307                } else {
308                    if let Ok(dir) = ::std::env::var("SOLANA_ABI_DUMP_DIR") {
309                        assert_eq!(#expected_digest, actual_digest, "Possibly ABI changed? Examine the diff in SOLANA_ABI_DUMP_DIR!: \n$ diff -u {}/*{}* {}/*{}*", dir, #expected_digest, dir, actual_digest);
310                    } else {
311                        assert_eq!(#expected_digest, actual_digest, "Possibly ABI changed? Confirm the diff by rerunning before and after this test failed with SOLANA_ABI_DUMP_DIR!");
312                    }
313                }
314            }
315        }
316    }
317}
318
319#[cfg(feature = "frozen-abi")]
320fn test_mod_name(type_name: &Ident) -> Ident {
321    Ident::new(&format!("{type_name}_frozen_abi"), Span::call_site())
322}
323
324#[cfg(feature = "frozen-abi")]
325fn frozen_abi_type_alias(input: ItemType, expected_digest: &str) -> TokenStream {
326    let type_name = &input.ident;
327    let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
328    let result = quote! {
329        #input
330        #test
331    };
332    result.into()
333}
334
335#[cfg(feature = "frozen-abi")]
336fn frozen_abi_struct_type(input: ItemStruct, expected_digest: &str) -> TokenStream {
337    let type_name = &input.ident;
338    let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
339    let result = quote! {
340        #input
341        #test
342    };
343    result.into()
344}
345
346#[cfg(feature = "frozen-abi")]
347fn quote_sample_variant(
348    type_name: &Ident,
349    ty_generics: &syn::TypeGenerics,
350    variant: &Variant,
351) -> TokenStream2 {
352    let variant_name = &variant.ident;
353    let variant = &variant.fields;
354    if *variant == Fields::Unit {
355        quote! {
356            let sample_variant: #type_name #ty_generics = #type_name::#variant_name;
357        }
358    } else if let Fields::Unnamed(variant_fields) = variant {
359        let mut fields = quote! {};
360        for field in &variant_fields.unnamed {
361            if !(field.ident.is_none() && field.colon_token.is_none()) {
362                unimplemented!();
363            }
364            let ty = &field.ty;
365            fields.extend(quote! {
366                <#ty>::example(),
367            });
368        }
369        quote! {
370            let sample_variant: #type_name #ty_generics = #type_name::#variant_name(#fields);
371        }
372    } else if let Fields::Named(variant_fields) = variant {
373        let mut fields = quote! {};
374        for field in &variant_fields.named {
375            if field.ident.is_none() || field.colon_token.is_none() {
376                unimplemented!();
377            }
378            let field_type_name = &field.ty;
379            let field_name = &field.ident;
380            fields.extend(quote! {
381                #field_name: <#field_type_name>::example(),
382            });
383        }
384        quote! {
385            let sample_variant: #type_name #ty_generics = #type_name::#variant_name{#fields};
386        }
387    } else {
388        unimplemented!("variant: {:?}", variant)
389    }
390}
391
392#[cfg(feature = "frozen-abi")]
393fn frozen_abi_enum_type(input: ItemEnum, expected_digest: &str) -> TokenStream {
394    let type_name = &input.ident;
395    let test = quote_for_test(&test_mod_name(type_name), type_name, expected_digest);
396    let result = quote! {
397        #input
398        #test
399    };
400    result.into()
401}
402
403#[cfg(feature = "frozen-abi")]
404#[proc_macro_attribute]
405pub fn frozen_abi(attrs: TokenStream, item: TokenStream) -> TokenStream {
406    let mut expected_digest: Option<String> = None;
407    let attrs_parser = syn::meta::parser(|meta| {
408        if meta.path.is_ident("digest") {
409            expected_digest = Some(meta.value()?.parse::<LitStr>()?.value());
410            Ok(())
411        } else {
412            Err(meta.error("unsupported \"frozen_abi\" property"))
413        }
414    });
415    parse_macro_input!(attrs with attrs_parser);
416
417    let Some(expected_digest) = expected_digest else {
418        return Error::new_spanned(
419            TokenStream2::from(item),
420            "the required \"digest\" = ... attribute is missing.",
421        )
422        .to_compile_error()
423        .into();
424    };
425
426    let item = parse_macro_input!(item as Item);
427    match item {
428        Item::Struct(input) => frozen_abi_struct_type(input, &expected_digest),
429        Item::Enum(input) => frozen_abi_enum_type(input, &expected_digest),
430        Item::Type(input) => frozen_abi_type_alias(input, &expected_digest),
431        _ => Error::new_spanned(
432            item,
433            "frozen_abi isn't applicable; only for struct, enum and type",
434        )
435        .to_compile_error()
436        .into(),
437    }
438}