1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4
5#[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 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 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 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}