use {
crate::{AccountField, AccountsStruct, Ty},
heck::ToSnakeCase,
quote::quote,
std::{collections::HashSet, str::FromStr},
};
pub fn generate(
accs: &AccountsStruct,
program_id: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
generate_with_opts(accs, program_id, &HashSet::new())
}
pub fn generate_with_opts(
accs: &AccountsStruct,
program_id: proc_macro2::TokenStream,
fieldless: &HashSet<String>,
) -> proc_macro2::TokenStream {
let name = &accs.ident;
#[allow(
clippy::unwrap_used,
reason = "computed from valid Rust identifier via snake_case"
)]
let account_mod_name: proc_macro2::TokenStream = format!(
"__cpi_client_accounts_{}",
accs.ident.to_string().to_snake_case()
)
.parse()
.unwrap();
let account_struct_fields: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &AccountField| match f {
AccountField::CompositeField(s) => {
let name = &s.ident;
let docs = if let Some(ref docs) = s.docs {
docs.iter()
.map(|docs_line| {
#[allow(clippy::unwrap_used, reason = "hardcoded valid doc comment syntax")]
let ts = proc_macro2::TokenStream::from_str(&format!(
"#[doc = r#\"{docs_line}\"#]"
))
.unwrap();
ts
})
.collect()
} else {
quote!()
};
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "symbol path is always non-empty and is a valid Rust path"
)]
let symbol: proc_macro2::TokenStream = {
let symbol_path = s.symbol.split("::").collect::<Vec<_>>();
let name = symbol_path
.last()
.expect("symbol path must have at least one segment");
let prefix = symbol_path
.get(..symbol_path.len().saturating_sub(1))
.unwrap_or(&[]);
let helper_mod = format!("__cpi_client_accounts_{}", name.to_snake_case());
if prefix.is_empty() {
format!("{helper_mod}::{name}")
} else {
format!("{}::{helper_mod}::{name}", prefix.join("::"),)
}
.parse()
.expect("generated module path must be valid Rust tokens")
};
let symbol_name = s.symbol.rsplit("::").next().unwrap_or(&s.symbol);
let lifetime = if fieldless.contains(symbol_name) {
quote!()
} else {
quote!(<'info>)
};
quote! {
#docs
pub #name: #symbol #lifetime
}
}
AccountField::Field(f) => {
let name = &f.ident;
let docs = if let Some(ref docs) = f.docs {
docs.iter()
.map(|docs_line| {
#[allow(clippy::unwrap_used, reason = "hardcoded valid doc comment syntax")]
let ts = proc_macro2::TokenStream::from_str(&format!(
"#[doc = r#\"{docs_line}\"#]"
))
.unwrap();
ts
})
.collect()
} else {
quote!()
};
if f.is_optional {
quote! {
#docs
pub #name: Option<anchor_lang::solana_program::account_info::AccountInfo<'info>>
}
} else {
quote! {
#docs
pub #name: anchor_lang::solana_program::account_info::AccountInfo<'info>
}
}
}
})
.collect();
let account_struct_metas: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &AccountField| match f {
AccountField::CompositeField(s) => {
let name = &s.ident;
quote! {
account_metas.extend(self.#name.to_account_metas(is_signer));
}
}
AccountField::Field(f) => {
let is_signer = match f.ty {
Ty::Signer => true,
_ => f.constraints.is_signer(),
};
let is_signer = match is_signer {
false => quote! {false},
true => quote! {is_signer.unwrap_or(true)},
};
let meta = match f.constraints.is_mutable() {
false => quote! { anchor_lang::solana_program::instruction::AccountMeta::new_readonly },
true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new },
};
let name = &f.ident;
if f.is_optional {
quote! {
if let Some(#name) = &self.#name {
account_metas.push(#meta(anchor_lang::Key::key(#name), #is_signer));
} else {
account_metas.push(anchor_lang::solana_program::instruction::AccountMeta::new_readonly(#program_id, false));
}
}
} else {
quote! {
account_metas.push(#meta(anchor_lang::Key::key(&self.#name), #is_signer));
}
}
}
})
.collect();
let account_struct_infos: Vec<proc_macro2::TokenStream> = accs
.fields
.iter()
.map(|f: &AccountField| {
let name = &f.ident();
quote! {
account_infos.extend(anchor_lang::ToAccountInfos::to_account_infos(&self.#name));
}
})
.collect();
let re_exports: Vec<proc_macro2::TokenStream> = {
let mut re_exports = std::collections::HashSet::new();
let mut re_exported_names = std::collections::HashSet::new();
for f in accs.fields.iter().filter_map(|f: &AccountField| match f {
AccountField::CompositeField(s) => Some(s),
AccountField::Field(_) => None,
}) {
let symbol_path = f.symbol.split("::").collect::<Vec<_>>();
let name = symbol_path.last().copied().unwrap_or_default();
if re_exported_names.contains(name) {
continue;
}
let prefix = symbol_path
.get(..symbol_path.len().saturating_sub(1))
.unwrap_or(&[]);
let helper_mod = format!("__cpi_client_accounts_{}", name.to_snake_case());
if prefix.is_empty() {
re_exports.insert(format!("{helper_mod}::{name}"));
} else {
re_exports.insert(format!("{}::{helper_mod}::{name}", prefix.join("::")));
}
re_exported_names.insert(name.to_string());
}
re_exports
.iter()
.map(|symbol: &String| {
#[allow(
clippy::unwrap_used,
reason = "symbol is a known-valid module path string"
)]
let symbol: proc_macro2::TokenStream = symbol.parse().unwrap();
quote! {
pub use #symbol;
}
})
.collect()
};
let omit_lifetime = fieldless.contains(&name.to_string());
let (phantom_field, extra_derives) = if account_struct_fields.is_empty() && !omit_lifetime {
(
quote! {
#[doc(hidden)]
pub __anchor_phantom: ::core::marker::PhantomData<&'info ()>
},
quote! { , Default },
)
} else {
(quote! {}, quote! {})
};
let generics = if omit_lifetime {
quote! {}
} else {
quote! {<'info>}
};
#[allow(clippy::unwrap_used, reason = "hardcoded valid doc comment syntax")]
let struct_doc = proc_macro2::TokenStream::from_str(&format!(
"#[doc = \" Generated CPI struct of the accounts for [`{name}`].\"]"
))
.unwrap();
quote! {
#[doc(hidden)]
pub mod #account_mod_name {
use super::*;
#(#re_exports)*
#struct_doc
#[derive(Debug, Clone #extra_derives)]
pub struct #name #generics {
#(#account_struct_fields,)*
#phantom_field
}
#[automatically_derived]
impl #generics anchor_lang::ToAccountMetas for #name #generics {
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<anchor_lang::solana_program::instruction::AccountMeta> {
let mut account_metas = vec![];
#(#account_struct_metas)*
account_metas
}
}
#[automatically_derived]
impl<'info> anchor_lang::ToAccountInfos<'info> for #name #generics {
fn to_account_infos(&self) -> Vec<anchor_lang::solana_program::account_info::AccountInfo<'info>> {
let mut account_infos = vec![];
#(#account_struct_infos)*
account_infos
}
}
}
}
}