Skip to main content

anchor_derive_serde/
lib.rs

1//! Defines the [`AnchorSerialize`] and [`AnchorDeserialize`] derive macros
2//! These emit a `BorshSerialize`/`BorshDeserialize` implementation for the given type,
3//! as well as emitting IDL type information when the `idl-build` feature is enabled.
4
5extern crate proc_macro;
6
7#[cfg(feature = "lazy-account")]
8mod lazy;
9
10#[cfg(feature = "lazy-account")]
11use syn::spanned::Spanned;
12use {
13    proc_macro::TokenStream,
14    proc_macro2::{Span, TokenStream as TokenStream2},
15    proc_macro_crate::FoundCrate,
16    quote::quote,
17    syn::{parse_macro_input, DeriveInput, Ident, Meta, NestedMeta},
18};
19
20/// Only one item-level `#[borsh]` attribute may be present, and we apply our own borsh attribute.
21/// Remove any user-provided `#[borsh]` attributes to apply in our generated derive.
22fn extract_borsh_attrs(input: &mut DeriveInput) -> Vec<NestedMeta> {
23    input
24        .attrs
25        .extract_if(.., |attr| attr.path.is_ident("borsh"))
26        .filter_map(|attr| {
27            if let Ok(Meta::List(list)) = attr.parse_meta() {
28                Some(list)
29            } else {
30                None
31            }
32        })
33        .flat_map(|list| list.nested)
34        .collect()
35}
36
37/// Locate any `#[borsh]` attributes on struct/enum fields,
38/// which are currently unsupported with `lazy-account`.
39#[cfg(feature = "lazy-account")]
40fn find_field_borsh_attr(input: &DeriveInput) -> Option<&syn::Attribute> {
41    match &input.data {
42        syn::Data::Struct(data) => data
43            .fields
44            .iter()
45            .flat_map(|field| field.attrs.iter())
46            .find(|attr| attr.path.is_ident("borsh")),
47        syn::Data::Enum(data) => data
48            .variants
49            .iter()
50            .flat_map(|variant| variant.fields.iter())
51            .flat_map(|field| field.attrs.iter())
52            .find(|attr| attr.path.is_ident("borsh")),
53        syn::Data::Union(data) => data
54            .fields
55            .named
56            .iter()
57            .flat_map(|field| field.attrs.iter())
58            .find(|attr| attr.path.is_ident("borsh")),
59    }
60}
61
62fn gen_borsh_serialize(input: TokenStream) -> TokenStream {
63    let mut item = parse_macro_input!(input as DeriveInput);
64    let borsh_attrs = extract_borsh_attrs(&mut item);
65    let attrs = helper_attrs("BorshSerialize", borsh_attrs);
66    quote! {
67        #attrs
68        #item
69    }
70    .into()
71}
72
73#[proc_macro_derive(AnchorSerialize, attributes(borsh))]
74pub fn anchor_serialize(input: TokenStream) -> TokenStream {
75    #[cfg(not(feature = "idl-build"))]
76    let ret = gen_borsh_serialize(input);
77    #[cfg(feature = "idl-build")]
78    let ret = gen_borsh_serialize(input.clone());
79
80    #[cfg(feature = "idl-build")]
81    {
82        use {anchor_syn::idl::*, quote::quote, syn::Item};
83
84        let idl_build_impl = match syn::parse(input).unwrap() {
85            Item::Struct(item) => impl_idl_build_struct(&item),
86            Item::Enum(item) => impl_idl_build_enum(&item),
87            Item::Union(item) => impl_idl_build_union(&item),
88            // Derive macros can only be defined on structs, enums, and unions.
89            _ => unreachable!(),
90        };
91
92        let ret = TokenStream2::from(ret);
93        return quote! {
94            #ret
95            #idl_build_impl
96        }
97        .into();
98    };
99
100    #[cfg(not(feature = "idl-build"))]
101    ret
102}
103
104fn gen_borsh_deserialize(input: TokenStream) -> TokenStream {
105    let mut item = parse_macro_input!(input as DeriveInput);
106    #[cfg(feature = "lazy-account")]
107    if let Some(attr) = find_field_borsh_attr(&item) {
108        return syn::Error::new(
109            attr.span(),
110            "`borsh` attributes are not currently supported with `lazy-account`",
111        )
112        .into_compile_error()
113        .into();
114    }
115
116    let borsh_attrs = extract_borsh_attrs(&mut item);
117    #[cfg(feature = "lazy-account")]
118    {
119        // `use_discriminant = false` is safe with `lazy-account` because it preserves
120        // borsh's default sequential tag encoding (0, 1, 2, ...) which Lazy's
121        // `size_of` relies on. `use_discriminant = true` would encode explicit
122        // discriminant values as the tag byte, breaking the Lazy match arms.
123        // Other item-level borsh attrs are not yet supported.
124        let unsupported = borsh_attrs.iter().find(|attr| {
125            !matches!(
126                attr,
127                NestedMeta::Meta(Meta::NameValue(nv))
128                    if nv.path.is_ident("use_discriminant")
129                        && matches!(&nv.lit, syn::Lit::Bool(b) if !b.value)
130            )
131        });
132        if let Some(attr) = unsupported {
133            return syn::Error::new(
134                attr.span(),
135                "only `#[borsh(use_discriminant = false)]` is supported with `lazy-account`; \
136                 `use_discriminant = true` and other `borsh` attributes are not yet supported",
137            )
138            .into_compile_error()
139            .into();
140        }
141    }
142    let attrs = helper_attrs("BorshDeserialize", borsh_attrs);
143    quote! {
144        #attrs
145        #item
146    }
147    .into()
148}
149
150/// Implements `borsh` deserialization for this structure, as well as implementing lazy
151/// deserialization if the `lazy-account` feature is enabled.
152/// `#[borsh(use_discriminant = false)]` is supported with `lazy-account`;
153/// `use_discriminant = true` and other `#[borsh]` attributes (e.g. `skip`) are not yet
154/// supported in conjunction with `lazy-account`.
155///
156/// ```
157/// # use anchor_derive_serde::AnchorDeserialize;
158/// #[derive(AnchorDeserialize)]
159/// #[borsh(use_discriminant = false)]
160/// pub enum Example {
161///     Foo = 1,
162///     Bar = 2,
163/// }
164/// ```
165///
166#[cfg_attr(feature = "lazy-account", doc = "```compile_fail")]
167#[cfg_attr(
168    feature = "lazy-account",
169    doc = "// Will not compile with `lazy-account`"
170)]
171#[cfg_attr(not(feature = "lazy-account"), doc = "```")]
172/// # use anchor_derive_serde::AnchorDeserialize;
173/// #[derive(AnchorDeserialize)]
174/// pub struct Example {
175///     #[borsh(skip)]
176///     x: u8,
177/// }
178/// ```
179#[proc_macro_derive(AnchorDeserialize, attributes(borsh))]
180pub fn anchor_deserialize(input: TokenStream) -> TokenStream {
181    #[cfg(feature = "lazy-account")]
182    {
183        let deser = TokenStream2::from(gen_borsh_deserialize(input.clone()));
184        let lazy = lazy::gen_lazy(input).unwrap_or_else(|e| e.to_compile_error());
185        quote! {
186            #deser
187            #lazy
188        }
189        .into()
190    }
191
192    #[cfg(not(feature = "lazy-account"))]
193    gen_borsh_deserialize(input)
194}
195
196fn helper_attrs(mac: &str, borsh_attrs: Vec<NestedMeta>) -> TokenStream2 {
197    // We need to emit the original borsh deserialization macros on our type,
198    // but derive macros can't emit other derives. To get around this, we use a hack:
199    // 1. Define an `__erase` attribute macro which deletes the item it is applied to
200    // 2. Emit a call to the derive, followed by a copy of the input struct with #[__erase] applied
201    // 3. This results in the trait implementations being produced, but the duplicate type definition being deleted
202
203    let mac_path = Ident::new(mac, Span::call_site());
204    let anchor = proc_macro_crate::crate_name("anchor-lang")
205        .expect("`anchor-derive-serde` must be used via `anchor-lang`");
206
207    let anchor_path = Ident::new(
208        match &anchor {
209            FoundCrate::Itself => "crate",
210            FoundCrate::Name(cr) => cr.as_str(),
211        },
212        Span::call_site(),
213    );
214    let borsh_path = quote! { #anchor_path::prelude::borsh };
215    let borsh_path_str = borsh_path.to_string();
216    quote! {
217        #[derive(#borsh_path::#mac_path)]
218        // Borsh derives used in a re-export require providing the path to `borsh`
219        #[borsh(crate = #borsh_path_str, #(#borsh_attrs),*)]
220        #[#anchor_path::__erase]
221    }
222}
223
224/// Deletes the item it is applied to. Implementation detail and not part of public API.
225#[doc(hidden)]
226#[proc_macro_attribute]
227pub fn __erase(_: TokenStream, _: TokenStream) -> TokenStream {
228    TokenStream::new()
229}
230
231#[cfg(feature = "lazy-account")]
232#[proc_macro_derive(Lazy)]
233pub fn lazy(input: TokenStream) -> TokenStream {
234    lazy::gen_lazy(input)
235        .unwrap_or_else(|e| e.to_compile_error())
236        .into()
237}