diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d453b64879..48218d62de 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -398,6 +398,8 @@ jobs: path: tests/pda-derivation - cmd: cd tests/anchor-cli-idl && ./test.sh path: tests/anchor-cli-idl + - cmd: cd tests/idl-generation && ./test.sh + path: tests/idl-generation steps: - uses: actions/checkout@v2 - uses: ./.github/actions/setup/ diff --git a/Cargo.lock b/Cargo.lock index f7ce8b7044..7818d3a541 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,7 @@ version = "0.24.2" dependencies = [ "anchor-syn", "proc-macro2 1.0.36", + "quote 1.0.15", "syn 1.0.88", ] @@ -212,8 +213,10 @@ dependencies = [ name = "anchor-derive-serde" version = "0.24.2" dependencies = [ + "anchor-syn", "borsh-derive-internal", "proc-macro2 1.0.36", + "quote 1.0.15", "syn 1.0.88", ] @@ -231,6 +234,7 @@ dependencies = [ "anchor-attribute-state", "anchor-derive-accounts", "anchor-derive-serde", + "anchor-syn", "arrayref", "base64 0.13.0", "bincode", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4d3dcba739..6146c975f1 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -22,7 +22,7 @@ anyhow = "1.0.32" syn = { version = "1.0.60", features = ["full", "extra-traits"] } anchor-lang = { path = "../lang", version = "0.24.2" } anchor-client = { path = "../client", version = "0.24.2" } -anchor-syn = { path = "../lang/syn", features = ["idl", "init-if-needed"], version = "0.24.2" } +anchor-syn = { path = "../lang/syn", features = ["idl-parse", "init-if-needed"], version = "0.24.2" } serde_json = "1.0" shellexpand = "2.1.0" toml = "0.5.8" diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 5592160ac2..c09d4717fb 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -5,7 +5,7 @@ use crate::config::{ use anchor_client::Cluster; use anchor_lang::idl::{IdlAccount, IdlInstruction, ERASED_AUTHORITY}; use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize}; -use anchor_syn::idl::types::Idl; +use anchor_syn::idl::types::{Idl, IdlConst, IdlErrorCode, IdlEvent, IdlTypeDefinition}; use anyhow::{anyhow, Context, Result}; use clap::Parser; use flate2::read::GzDecoder; @@ -374,6 +374,11 @@ pub enum IdlCommand { #[clap(long)] no_docs: bool, }, + /// Generates the IDL for the program using the compilation method. + Build { + #[clap(long)] + no_docs: bool, + }, /// Fetches an IDL for the given address from a cluster. /// The address can be a program, IDL account, or IDL buffer. Fetch { @@ -1546,6 +1551,7 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { out_ts, no_docs, } => idl_parse(cfg_override, file, out, out_ts, no_docs), + IdlCommand::Build { no_docs } => idl_build(no_docs), IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out), } } @@ -1825,6 +1831,164 @@ fn idl_parse( Ok(()) } +fn idl_build(no_docs: bool) -> Result<()> { + let no_docs = if no_docs { "TRUE" } else { "FALSE" }; + + let exit = std::process::Command::new("cargo") + .args([ + "test", + "__anchor_private_print_idl", + "--features", + "idl-gen", + "--", + "--show-output", + "--quiet", + ]) + .env("ANCHOR_IDL_GEN_NO_DOCS", no_docs) + .stderr(Stdio::inherit()) + .output() + .map_err(|e| anyhow::format_err!("{}", e.to_string()))?; + if !exit.status.success() { + std::process::exit(exit.status.code().unwrap_or(1)); + } + + enum State { + Pass, + ConstLines(Vec), + EventLines(Vec), + ErrorsLines(Vec), + ProgramLines(Vec), + } + + #[derive(Serialize, Deserialize)] + struct IdlGenEventPrint { + event: IdlEvent, + defined_types: Vec, + } + + let mut state = State::Pass; + + let mut events: Vec = vec![]; + let mut error_codes: Option> = None; + let mut constants: Vec = vec![]; + let mut defined_types: BTreeMap = BTreeMap::new(); + let mut curr_idl: Option = None; + + let mut idls: Vec = vec![]; + + let out = String::from_utf8_lossy(&exit.stdout); + for line in out.lines() { + match &mut state { + State::Pass => { + if line == "---- IDL begin const ----" { + state = State::ConstLines(vec![]); + continue; + } else if line == "---- IDL begin event ----" { + state = State::EventLines(vec![]); + continue; + } else if line == "---- IDL begin errors ----" { + state = State::ErrorsLines(vec![]); + continue; + } else if line == "---- IDL begin program ----" { + state = State::ProgramLines(vec![]); + continue; + } else if line.starts_with("test result: ok") { + let events = std::mem::take(&mut events); + let error_codes = error_codes.take(); + let constants = std::mem::take(&mut constants); + let mut defined_types = std::mem::take(&mut defined_types); + let curr_idl = curr_idl.take(); + + let events = if !events.is_empty() { + Some(events) + } else { + None + }; + + let mut idl = match curr_idl { + Some(idl) => idl, + None => continue, + }; + + idl.events = events; + idl.errors = error_codes; + idl.constants = constants; + + idl.constants.sort_by(|a, b| a.name.cmp(&b.name)); + idl.accounts.sort_by(|a, b| a.name.cmp(&b.name)); + if let Some(e) = idl.events.as_mut() { + e.sort_by(|a, b| a.name.cmp(&b.name)) + } + + let prog_ty = std::mem::take(&mut idl.types); + defined_types.extend( + prog_ty + .into_iter() + .map(|ty| (ty.full_path.clone().unwrap(), ty)), + ); + idl.types = defined_types.into_values().collect::>(); + + idls.push(idl); + continue; + } + } + State::ConstLines(lines) => { + if line == "---- IDL end const ----" { + let constant: IdlConst = serde_json::from_str(&lines.join("\n"))?; + constants.push(constant); + state = State::Pass; + continue; + } + lines.push(line.to_string()); + } + State::EventLines(lines) => { + if line == "---- IDL end event ----" { + let event: IdlGenEventPrint = serde_json::from_str(&lines.join("\n"))?; + events.push(event.event); + defined_types.extend( + event + .defined_types + .into_iter() + .map(|ty| (ty.full_path.clone().unwrap(), ty)), + ); + state = State::Pass; + continue; + } + lines.push(line.to_string()); + } + State::ErrorsLines(lines) => { + if line == "---- IDL end errors ----" { + let errs: Vec = serde_json::from_str(&lines.join("\n"))?; + error_codes = Some(errs); + state = State::Pass; + continue; + } + lines.push(line.to_string()); + } + State::ProgramLines(lines) => { + if line == "---- IDL end program ----" { + let idl: Idl = serde_json::from_str(&lines.join("\n"))?; + curr_idl = Some(idl); + state = State::Pass; + continue; + } + lines.push(line.to_string()); + } + } + } + + if idls.len() == 1 { + println!( + "{}", + serde_json::to_string_pretty(&idls.first().unwrap()).unwrap() + ); + } else if idls.len() >= 2 { + println!("{}", serde_json::to_string_pretty(&idls).unwrap()); + }; + + Ok(()) +} + fn idl_fetch(cfg_override: &ConfigOverride, address: Pubkey, out: Option) -> Result<()> { let idl = fetch_idl(cfg_override, address)?; let out = match out { diff --git a/lang/Cargo.toml b/lang/Cargo.toml index ad92780822..fe4b8101d0 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -12,6 +12,16 @@ description = "Solana Sealevel eDSL" init-if-needed = ["anchor-derive-accounts/init-if-needed"] derive = [] default = [] +idl-gen = [ + "anchor-syn/idl-gen", + "anchor-derive-accounts/idl-gen", + "anchor-derive-serde/idl-gen", + "anchor-attribute-account/idl-gen", + "anchor-attribute-constant/idl-gen", + "anchor-attribute-event/idl-gen", + "anchor-attribute-error/anchor-debug", + "anchor-attribute-program/idl-gen", +] anchor-debug = [ "anchor-attribute-access-control/anchor-debug", "anchor-attribute-account/anchor-debug", @@ -36,6 +46,9 @@ anchor-attribute-interface = { path = "./attribute/interface", version = "0.24.2 anchor-attribute-event = { path = "./attribute/event", version = "0.24.2" } anchor-derive-accounts = { path = "./derive/accounts", version = "0.24.2" } anchor-derive-serde = { path = "./derive/serde", version = "0.24.2" } +# anchor-syn can and should only be included only for idl-gen. It won't compile +# for bpf due to proc-macro2 crate. +anchor-syn = { path = "./syn", version = "0.24.2", optional = true } arrayref = "0.3.6" base64 = "0.13.0" borsh = "0.9" diff --git a/lang/attribute/account/Cargo.toml b/lang/attribute/account/Cargo.toml index 215812bd57..984aecd429 100644 --- a/lang/attribute/account/Cargo.toml +++ b/lang/attribute/account/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" proc-macro = true [features] +idl-gen = ["anchor-syn/idl-gen"] anchor-debug = ["anchor-syn/anchor-debug"] [dependencies] diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 8692d1fc18..e0f13f337e 100644 --- a/lang/attribute/account/src/lib.rs +++ b/lang/attribute/account/src/lib.rs @@ -1,5 +1,7 @@ extern crate proc_macro; +#[cfg(feature = "idl-gen")] +use anchor_syn::idl::gen::*; use quote::quote; use syn::parse_macro_input; @@ -305,11 +307,24 @@ pub fn zero_copy( None => quote! {#[repr(C)]}, }; - proc_macro::TokenStream::from(quote! { + let ret = quote! { #[derive(anchor_lang::__private::ZeroCopyAccessor, Copy, Clone)] #repr #account_strct - }) + }; + + #[cfg(feature = "idl-gen")] + { + let no_docs = get_no_docs(); + let idl_gen_impl = gen_idl_gen_impl_for_struct(&account_strct, no_docs); + return proc_macro::TokenStream::from(quote! { + #ret + #idl_gen_impl + }); + } + + #[allow(unreachable_code)] + proc_macro::TokenStream::from(ret) } /// Defines the program's ID. This should be used at the root of all Anchor diff --git a/lang/attribute/constant/Cargo.toml b/lang/attribute/constant/Cargo.toml index 4a80407c9f..7e7b050547 100644 --- a/lang/attribute/constant/Cargo.toml +++ b/lang/attribute/constant/Cargo.toml @@ -12,9 +12,11 @@ edition = "2021" proc-macro = true [features] +idl-gen = ["anchor-syn/idl-gen"] anchor-debug = ["anchor-syn/anchor-debug"] [dependencies] proc-macro2 = "1.0" syn = { version = "1.0.60", features = ["full"] } anchor-syn = { path = "../../syn", version = "0.24.2" } +quote = "1.0" diff --git a/lang/attribute/constant/src/lib.rs b/lang/attribute/constant/src/lib.rs index 98092f4b0f..190ff68e77 100644 --- a/lang/attribute/constant/src/lib.rs +++ b/lang/attribute/constant/src/lib.rs @@ -1,5 +1,8 @@ extern crate proc_macro; +#[cfg(feature = "idl-gen")] +use {anchor_syn::idl::gen::gen_idl_print_function_for_constant, quote::quote, syn}; + /// A marker attribute used to mark const values that should be included in the /// generated IDL but functionally does nothing. #[proc_macro_attribute] @@ -7,5 +10,24 @@ pub fn constant( _attr: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { + #[cfg(feature = "idl-gen")] + { + let ts = match syn::parse(input).unwrap() { + syn::Item::Const(item) => { + let idl_print = gen_idl_print_function_for_constant(&item); + quote! { + #item + #idl_print + } + } + item => quote! {#item}, + }; + + return proc_macro::TokenStream::from(quote! { + #ts + }); + }; + + #[allow(unreachable_code)] input } diff --git a/lang/attribute/error/Cargo.toml b/lang/attribute/error/Cargo.toml index 5d8d62447c..638b07c28e 100644 --- a/lang/attribute/error/Cargo.toml +++ b/lang/attribute/error/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" proc-macro = true [features] +idl-gen = ["anchor-syn/idl-gen"] anchor-debug = ["anchor-syn/anchor-debug"] [dependencies] diff --git a/lang/attribute/event/Cargo.toml b/lang/attribute/event/Cargo.toml index e668b3c459..fb386ab6ad 100644 --- a/lang/attribute/event/Cargo.toml +++ b/lang/attribute/event/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" proc-macro = true [features] +idl-gen = ["anchor-syn/idl-gen"] anchor-debug = ["anchor-syn/anchor-debug"] [dependencies] diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index cabe676ddc..9b117e171f 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -27,7 +27,7 @@ pub fn event( format!("{:?}", discriminator).parse().unwrap() }; - proc_macro::TokenStream::from(quote! { + let ret = quote! { #[derive(anchor_lang::__private::EventIndex, AnchorSerialize, AnchorDeserialize)] #event_strct @@ -44,7 +44,19 @@ pub fn event( #discriminator } } - }) + }; + + #[cfg(feature = "idl-gen")] + { + let idl_gen = anchor_syn::idl::gen::gen_idl_print_function_for_event(&event_strct); + return proc_macro::TokenStream::from(quote! { + #ret + #idl_gen + }); + } + + #[allow(unreachable_code)] + proc_macro::TokenStream::from(ret) } /// Logs an event that can be subscribed to by clients. diff --git a/lang/attribute/program/Cargo.toml b/lang/attribute/program/Cargo.toml index ee35d288c4..1c58e9b7e1 100644 --- a/lang/attribute/program/Cargo.toml +++ b/lang/attribute/program/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" proc-macro = true [features] +idl-gen = ["anchor-syn/idl-gen"] anchor-debug = ["anchor-syn/anchor-debug"] [dependencies] diff --git a/lang/derive/accounts/Cargo.toml b/lang/derive/accounts/Cargo.toml index a629d41bd9..d6a27bdcb6 100644 --- a/lang/derive/accounts/Cargo.toml +++ b/lang/derive/accounts/Cargo.toml @@ -14,6 +14,7 @@ proc-macro = true [features] init-if-needed = ["anchor-syn/init-if-needed"] default = [] +idl-gen = ["anchor-syn/idl-gen"] anchor-debug = ["anchor-syn/anchor-debug"] [dependencies] diff --git a/lang/derive/serde/Cargo.toml b/lang/derive/serde/Cargo.toml index 3824f15606..bb633a4321 100644 --- a/lang/derive/serde/Cargo.toml +++ b/lang/derive/serde/Cargo.toml @@ -12,9 +12,13 @@ edition = "2021" proc-macro = true [features] +idl-gen = [ + "anchor-syn/idl-gen", +] [dependencies] proc-macro2 = "1.0" borsh-derive-internal = "0.9" syn = { version = "1.0.60", features = ["full"] } - +anchor-syn = { path = "../../syn", version = "0.24.2" } +quote = "1.0" diff --git a/lang/derive/serde/src/lib.rs b/lang/derive/serde/src/lib.rs index eaee6bb1cf..81b2df8332 100644 --- a/lang/derive/serde/src/lib.rs +++ b/lang/derive/serde/src/lib.rs @@ -2,11 +2,13 @@ extern crate proc_macro; use borsh_derive_internal::*; use proc_macro::TokenStream; -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream as TokenStream2}; use syn::{Ident, Item}; -#[proc_macro_derive(AnchorSerialize, attributes(borsh_skip))] -pub fn anchor_serialize(input: TokenStream) -> TokenStream { +#[cfg(feature = "idl-gen")] +use {anchor_syn::idl::gen::*, quote::quote}; + +fn gen_borsh_serialize(input: TokenStream) -> TokenStream2 { let cratename = Ident::new("borsh", Span::call_site()); let item: Item = syn::parse(input).unwrap(); @@ -18,14 +20,45 @@ pub fn anchor_serialize(input: TokenStream) -> TokenStream { _ => unreachable!(), }; - TokenStream::from(match res { + match res { Ok(res) => res, Err(err) => err.to_compile_error(), - }) + } } -#[proc_macro_derive(AnchorDeserialize, attributes(borsh_skip, borsh_init))] -pub fn borsh_deserialize(input: TokenStream) -> TokenStream { +#[proc_macro_derive(AnchorSerialize, attributes(borsh_skip))] +pub fn anchor_serialize(input: TokenStream) -> TokenStream { + #[cfg(not(feature = "idl-gen"))] + let ret = gen_borsh_serialize(input); + #[cfg(feature = "idl-gen")] + let ret = gen_borsh_serialize(input.clone()); + + #[cfg(feature = "idl-gen")] + { + let no_docs = get_no_docs(); + + let idl_gen_impl = match syn::parse(input).unwrap() { + Item::Struct(item) => gen_idl_gen_impl_for_struct(&item, no_docs), + Item::Enum(item) => gen_idl_gen_impl_for_enum(item, no_docs), + Item::Union(item) => { + // unions are not included in the IDL - TODO print a warning + idl_gen_impl_skeleton(quote! {None}, quote! {}, &item.ident, &item.generics) + } + // Derive macros can only be defined on structs, enums, and unions. + _ => unreachable!(), + }; + + return TokenStream::from(quote! { + #ret + #idl_gen_impl + }); + }; + + #[allow(unreachable_code)] + TokenStream::from(ret) +} + +fn gen_borsh_deserialize(input: TokenStream) -> TokenStream2 { let cratename = Ident::new("borsh", Span::call_site()); let item: Item = syn::parse(input).unwrap(); @@ -37,8 +70,13 @@ pub fn borsh_deserialize(input: TokenStream) -> TokenStream { _ => unreachable!(), }; - TokenStream::from(match res { + match res { Ok(res) => res, Err(err) => err.to_compile_error(), - }) + } +} + +#[proc_macro_derive(AnchorDeserialize, attributes(borsh_skip, borsh_init))] +pub fn borsh_deserialize(input: TokenStream) -> TokenStream { + TokenStream::from(gen_borsh_deserialize(input)) } diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 9c20837151..43fc50ae82 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -59,6 +59,9 @@ pub use borsh::de::BorshDeserialize as AnchorDeserialize; pub use borsh::ser::BorshSerialize as AnchorSerialize; pub use solana_program; +#[cfg(feature = "idl-gen")] +pub use anchor_syn; + pub type Result = std::result::Result; /// A data structure of validated accounts that can be deserialized from the diff --git a/lang/syn/Cargo.toml b/lang/syn/Cargo.toml index de4594e452..ae4d468a6d 100644 --- a/lang/syn/Cargo.toml +++ b/lang/syn/Cargo.toml @@ -10,7 +10,9 @@ edition = "2021" [features] init-if-needed = [] -idl = [] +idl-gen = [] +idl-parse = [] +idl-types = [] hash = [] default = [] anchor-debug = [] diff --git a/lang/syn/src/codegen/accounts/mod.rs b/lang/syn/src/codegen/accounts/mod.rs index 3a239cbe33..77b4554fb0 100644 --- a/lang/syn/src/codegen/accounts/mod.rs +++ b/lang/syn/src/codegen/accounts/mod.rs @@ -22,7 +22,7 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { let __client_accounts_mod = __client_accounts::generate(accs); let __cpi_client_accounts_mod = __cpi_client_accounts::generate(accs); - quote! { + let ret = quote! { #impl_try_accounts #impl_to_account_infos #impl_to_account_metas @@ -30,7 +30,21 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream { #__client_accounts_mod #__cpi_client_accounts_mod + }; + + #[cfg(feature = "idl-gen")] + { + #![allow(warnings)] + let no_docs = crate::idl::gen::get_no_docs(); + let idl_gen_impl = crate::idl::gen::gen_idl_gen_impl_for_accounts_strct(&accs, no_docs); + return quote! { + #ret + #idl_gen_impl + }; } + + #[allow(unreachable_code)] + ret } fn generics(accs: &AccountsStruct) -> ParsedGenerics { diff --git a/lang/syn/src/codegen/error.rs b/lang/syn/src/codegen/error.rs index 4dea4ea2b7..a4735c755b 100644 --- a/lang/syn/src/codegen/error.rs +++ b/lang/syn/src/codegen/error.rs @@ -1,6 +1,9 @@ use crate::Error; use quote::quote; +#[cfg(feature = "idl-gen")] +use crate::idl::gen::gen_idl_print_function_for_error; + pub fn generate(error: Error) -> proc_macro2::TokenStream { let error_enum = &error.raw_enum; let enum_name = &error.ident; @@ -47,7 +50,7 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream { }) .collect(); - let offset = match error.args { + let offset = match &error.args { None => quote! { anchor_lang::error::ERROR_CODE_OFFSET}, Some(args) => { let offset = &args.offset; @@ -55,7 +58,7 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream { } }; - quote! { + let ret = quote! { #[derive(std::fmt::Debug, Clone, Copy)] #[repr(u32)] #error_enum @@ -96,5 +99,17 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream { } } } - } + }; + + #[cfg(feature = "idl-gen")] + { + let idl_gen = gen_idl_print_function_for_error(&error); + return quote! { + #ret + #idl_gen + }; + }; + + #[allow(unreachable_code)] + ret } diff --git a/lang/syn/src/codegen/program/mod.rs b/lang/syn/src/codegen/program/mod.rs index ea5aff0c1b..6517ca56a7 100644 --- a/lang/syn/src/codegen/program/mod.rs +++ b/lang/syn/src/codegen/program/mod.rs @@ -20,16 +20,33 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let cpi = cpi::generate(program); let accounts = accounts::generate(program); - quote! { - // TODO: remove once we allow segmented paths in `Accounts` structs. - use self::#mod_name::*; - - #entry - #dispatch - #handlers - #user_defined_program - #instruction - #cpi - #accounts - } + #[allow(clippy::let_and_return)] + let ret = { + quote! { + // TODO: remove once we allow segmented paths in `Accounts` structs. + use self::#mod_name::*; + + #entry + #dispatch + #handlers + #user_defined_program + #instruction + #cpi + #accounts + } + }; + + #[cfg(feature = "idl-gen")] + { + let no_docs = crate::idl::gen::get_no_docs(); + let idl_gen = crate::idl::gen::gen_idl_print_function_for_program(program, no_docs); + + return quote! { + #ret + #idl_gen + }; + }; + + #[allow(unreachable_code)] + ret } diff --git a/lang/syn/src/idl/gen.rs b/lang/syn/src/idl/gen.rs new file mode 100644 index 0000000000..d499b5fd7b --- /dev/null +++ b/lang/syn/src/idl/gen.rs @@ -0,0 +1,806 @@ +use crate::{parser::docs, AccountField, AccountsStruct, Error, Program}; +use heck::MixedCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +pub use serde_json; +use syn::{Ident, ItemEnum, ItemStruct}; + +#[inline(always)] +fn get_module_paths() -> (TokenStream, TokenStream) { + ( + quote!(anchor_lang::anchor_syn::idl::types), + quote!(anchor_lang::anchor_syn::idl::gen::serde_json), + ) +} + +#[inline(always)] +pub fn get_no_docs() -> bool { + std::option_env!("ANCHOR_IDL_GEN_NO_DOCS") + .map(|val| val == "TRUE") + .unwrap_or(false) +} + +// Returns TokenStream for IdlType enum and the syn::TypePath for the defined +// type if any. +// Returns Err when the type wasn't parsed successfully. +#[allow(clippy::result_unit_err)] +pub fn idl_type_ts_from_syn_type( + ty: &syn::Type, +) -> Result<(TokenStream, Option<&syn::TypePath>), ()> { + let (idl, _) = get_module_paths(); + + fn the_only_segment_is(path: &syn::TypePath, cmp: &str) -> bool { + if path.path.segments.len() != 1 { + return false; + }; + return path.path.segments.first().unwrap().ident == cmp; + } + + // Foo -> first::path + fn get_first_angle_bracketed_path_arg(segment: &syn::PathSegment) -> Option<&syn::Type> { + match &segment.arguments { + syn::PathArguments::AngleBracketed(arguments) => match arguments.args.first() { + Some(syn::GenericArgument::Type(ty)) => Some(ty), + _ => None, + }, + _ => None, + } + } + + match ty { + syn::Type::Path(path) if the_only_segment_is(path, "bool") => { + Ok((quote! { #idl::IdlType::Bool }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "u8") => { + Ok((quote! { #idl::IdlType::U8 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "i8") => { + Ok((quote! { #idl::IdlType::I8 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "u16") => { + Ok((quote! { #idl::IdlType::U16 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "i16") => { + Ok((quote! { #idl::IdlType::I16 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "u32") => { + Ok((quote! { #idl::IdlType::U32 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "i32") => { + Ok((quote! { #idl::IdlType::I32 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "f32") => { + Ok((quote! { #idl::IdlType::F32 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "u64") => { + Ok((quote! { #idl::IdlType::U64 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "i64") => { + Ok((quote! { #idl::IdlType::I64 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "f64") => { + Ok((quote! { #idl::IdlType::F64 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "u128") => { + Ok((quote! { #idl::IdlType::U128 }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "i128") => { + Ok((quote! { #idl::IdlType::I128 }, None)) + } + syn::Type::Path(path) + if the_only_segment_is(path, "String") || the_only_segment_is(path, "&str") => + { + Ok((quote! { #idl::IdlType::String }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "Pubkey") => { + Ok((quote! { #idl::IdlType::PublicKey }, None)) + } + syn::Type::Path(path) if the_only_segment_is(path, "Vec") => { + let segment = path.path.segments.first().unwrap(); + let arg = match get_first_angle_bracketed_path_arg(segment) { + Some(arg) => arg, + None => unreachable!("Vec arguments can only be of AngleBracketed variant"), + }; + match arg { + syn::Type::Path(path) if the_only_segment_is(path, "u8") => { + return Ok((quote! {#idl::IdlType::Bytes}, None)); + } + _ => (), + }; + let (inner, defined) = idl_type_ts_from_syn_type(arg)?; + Ok((quote! { #idl::IdlType::Vec(Box::new(#inner)) }, defined)) + } + syn::Type::Path(path) if the_only_segment_is(path, "Option") => { + let segment = path.path.segments.first().unwrap(); + let arg = match get_first_angle_bracketed_path_arg(segment) { + Some(arg) => arg, + None => unreachable!("Option arguments can only be of AngleBracketed variant"), + }; + let (inner, defined) = idl_type_ts_from_syn_type(arg)?; + Ok((quote! { #idl::IdlType::Option(Box::new(#inner)) }, defined)) + } + syn::Type::Path(path) if the_only_segment_is(path, "Box") => { + let segment = path.path.segments.first().unwrap(); + let arg = match get_first_angle_bracketed_path_arg(segment) { + Some(arg) => arg, + None => unreachable!("Box arguments can only be of AngleBracketed variant"), + }; + let (ts, defined) = idl_type_ts_from_syn_type(arg)?; + Ok((quote! { #ts }, defined)) + } + syn::Type::Array(arr) => { + let len = arr.len.clone(); + let (inner, defined) = idl_type_ts_from_syn_type(&arr.elem)?; + Ok(( + quote! { #idl::IdlType::Array(Box::new(#inner), #len) }, + defined, + )) + } + syn::Type::Path(path) => Ok(( + quote! { #idl::IdlType::Defined(#path::__anchor_private_full_path())}, + Some(path), + )), + _ => Err(()), + } +} + +// Returns TokenStream for IdlField struct and the syn::TypePath for the defined +// type if any. +// Returns Err when the type wasn't parsed successfully +#[allow(clippy::result_unit_err)] +pub fn idl_field_ts_from_syn_field( + field: &syn::Field, + no_docs: bool, +) -> Result<(TokenStream, Option<&syn::TypePath>), ()> { + let (idl, _) = get_module_paths(); + + let name = field.ident.as_ref().unwrap().to_string().to_mixed_case(); + let docs = match docs::parse(&field.attrs) { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + let (ty, defined) = idl_type_ts_from_syn_type(&field.ty)?; + + Ok(( + quote! { + #idl::IdlField { + name: #name.into(), + docs: #docs, + ty: #ty, + } + }, + defined, + )) +} + +// Returns TokenStream for IdlEventField struct and the syn::TypePath for the defined +// type if any. +// Returns Err when the type wasn't parsed successfully +#[allow(clippy::result_unit_err)] +pub fn idl_event_field_ts_from_syn_field( + field: &syn::Field, +) -> Result<(TokenStream, Option<&syn::TypePath>), ()> { + let (idl, _) = get_module_paths(); + + let name = field.ident.as_ref().unwrap().to_string().to_mixed_case(); + let (ty, defined) = idl_type_ts_from_syn_type(&field.ty)?; + + let index: bool = field + .attrs + .get(0) + .and_then(|attr| attr.path.segments.first()) + .map(|segment| segment.ident == "index") + .unwrap_or(false); + + Ok(( + quote! { + #idl::IdlEventField { + name: #name.into(), + ty: #ty, + index: #index, + } + }, + defined, + )) +} + +// Returns TokenStream for IdlTypeDefinitionTy::Struct and Vec<&syn::TypePath> +// for the defined types if any. +// Returns Err if any of the fields weren't parsed successfully. +#[allow(clippy::result_unit_err)] +pub fn idl_type_definition_ts_from_syn_struct( + item_strct: &syn::ItemStruct, + no_docs: bool, +) -> Result<(TokenStream, Vec<&syn::TypePath>), ()> { + let (idl, _) = get_module_paths(); + + let name = item_strct.ident.to_string(); + let docs = match docs::parse(&item_strct.attrs) { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + + let (fields, defined): (Vec, Vec>) = + match &item_strct.fields { + syn::Fields::Named(fields) => fields + .named + .iter() + .map(|f: &syn::Field| idl_field_ts_from_syn_field(f, no_docs)) + .collect::, _>>()? + .into_iter() + .unzip::<_, _, Vec<_>, Vec<_>>(), + _ => return Err(()), + }; + let defined = defined + .into_iter() + .flatten() + .collect::>(); + + Ok(( + quote! { + #idl::IdlTypeDefinition { + name: #name.into(), + full_path: Some(Self::__anchor_private_full_path()), + docs: #docs, + ty: #idl::IdlTypeDefinitionTy::Struct{ + fields: vec![ + #(#fields),* + ] + } + }, + }, + defined, + )) +} + +// Returns TokenStream for IdlTypeDefinitionTy::Enum and the Vec<&syn::TypePath> +// for the defined types if any. +// Returns Err if any of the fields didn't parse successfully. +#[allow(clippy::result_unit_err)] +pub fn idl_type_definition_ts_from_syn_enum( + enum_item: &syn::ItemEnum, + no_docs: bool, +) -> Result<(TokenStream, Vec<&syn::TypePath>), ()> { + let (idl, _) = get_module_paths(); + + let name = enum_item.ident.to_string(); + let docs = match docs::parse(&enum_item.attrs) { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + + let (variants, defined): (Vec, Vec>) = enum_item.variants.iter().map(|variant: &syn::Variant| { + let name = variant.ident.to_string(); + let (fields, defined): (TokenStream, Vec<&syn::TypePath>) = match &variant.fields { + syn::Fields::Unit => (quote!{None}, vec![]), + syn::Fields::Unnamed(fields) => { + let (types, defined) = fields.unnamed + .iter() + .map(|f| idl_type_ts_from_syn_type(&f.ty)) + .collect::, _>>()? + .into_iter() + .unzip::, Vec, Vec>>(); + let defined = defined + .into_iter() + .flatten() + .collect::>(); + + (quote!{ Some(#idl::EnumFields::Tuple(vec![#(#types),*]))}, defined) + } + syn::Fields::Named(fields) => { + let (fields, defined) = fields.named + .iter() + .map(|f| idl_field_ts_from_syn_field(f, no_docs)) + .collect::, _>>()? + .into_iter() + .unzip::, Vec, Vec>>(); + let defined = defined + .into_iter() + .flatten() + .collect::>(); + + (quote!{ Some(#idl::EnumFields::Named(vec![#(#fields),*]))}, defined) + } + }; + + Ok((quote!{ #idl::IdlEnumVariant{ name: #name.into(), fields: #fields }}, defined)) + }) + .collect::, _>>()? + .into_iter() + .unzip::, Vec, Vec>>(); + + let defined = defined + .into_iter() + .flatten() + .collect::>(); + + Ok(( + quote! { + #idl::IdlTypeDefinition { + name: #name.into(), + full_path: Some(Self::__anchor_private_full_path()), + docs: #docs, + ty: #idl::IdlTypeDefinitionTy::Enum{ + variants: vec![ + #(#variants),* + ] + } + } + }, + defined, + )) +} + +pub fn idl_gen_impl_skeleton( + idl_type_definition_ts: TokenStream, + insert_defined_ts: TokenStream, + ident: &Ident, + input_generics: &syn::Generics, +) -> TokenStream { + let (idl, _) = get_module_paths(); + let name = ident.to_string(); + let (impl_generics, ty_generics, where_clause) = input_generics.split_for_impl(); + + quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn __anchor_private_full_path() -> String { + format!("{}::{}", std::module_path!(), #name) + } + + pub fn __anchor_private_gen_idl_type() -> Option<#idl::IdlTypeDefinition> { + #idl_type_definition_ts + } + + pub fn __anchor_private_insert_idl_defined( + defined_types: &mut std::collections::HashMap + ) { + #insert_defined_ts + } + } + } +} + +// generates the IDL generation impl for for a struct +pub fn gen_idl_gen_impl_for_struct(strct: &ItemStruct, no_docs: bool) -> TokenStream { + let idl_type_definition_ts: TokenStream; + let insert_defined_ts: TokenStream; + + if let Ok((ts, defined)) = idl_type_definition_ts_from_syn_struct(strct, no_docs) { + idl_type_definition_ts = quote! {Some(#ts)}; + insert_defined_ts = quote! { + #({ + #defined::__anchor_private_insert_idl_defined(defined_types); + + let path = #defined::__anchor_private_full_path(); + #defined::__anchor_private_gen_idl_type() + .and_then(|ty| defined_types.insert(path, ty)); + });* + }; + } else { + idl_type_definition_ts = quote! {None}; + insert_defined_ts = quote! {}; + } + + let ident = &strct.ident; + let input_generics = &strct.generics; + + idl_gen_impl_skeleton( + idl_type_definition_ts, + insert_defined_ts, + ident, + input_generics, + ) +} + +// generates the IDL generation impl for for an enum +pub fn gen_idl_gen_impl_for_enum(enm: ItemEnum, no_docs: bool) -> TokenStream { + let idl_type_definition_ts: TokenStream; + let insert_defined_ts: TokenStream; + + if let Ok((ts, defined)) = idl_type_definition_ts_from_syn_enum(&enm, no_docs) { + idl_type_definition_ts = quote! {Some(#ts)}; + insert_defined_ts = quote! { + #({ + #defined::__anchor_private_insert_idl_defined(defined_types); + + let path = #defined::__anchor_private_full_path(); + #defined::__anchor_private_gen_idl_type() + .and_then(|ty| defined_types.insert(path, ty)); + });* + }; + } else { + idl_type_definition_ts = quote! {None}; + insert_defined_ts = quote! {}; + } + + let ident = &enm.ident; + let input_generics = &enm.generics; + + idl_gen_impl_skeleton( + idl_type_definition_ts, + insert_defined_ts, + ident, + input_generics, + ) +} + +// generates the IDL generation impl for for an event +pub fn gen_idl_gen_impl_for_event(event_strct: &ItemStruct) -> TokenStream { + fn parse_fields( + fields: &syn::FieldsNamed, + ) -> Result<(Vec, Vec<&syn::TypePath>), ()> { + let (fields, defined) = fields + .named + .iter() + .map(idl_event_field_ts_from_syn_field) + .collect::, _>>()? + .into_iter() + .unzip::<_, _, Vec<_>, Vec<_>>(); + let defined = defined + .into_iter() + .flatten() + .collect::>(); + + Ok((fields, defined)) + } + + let res = match &event_strct.fields { + syn::Fields::Named(fields) => parse_fields(fields), + _ => Err(()), + }; + + let (idl, _) = get_module_paths(); + let name = event_strct.ident.to_string(); + + let (ret_ts, types_ts) = match res { + Ok((fields, defined)) => { + let ret_ts = quote! { + Some( + #idl::IdlEvent { + name: #name.into(), + fields: vec![#(#fields),*], + } + ) + }; + let types_ts = quote! { + #({ + #defined::__anchor_private_insert_idl_defined(defined_types); + + let path = #defined::__anchor_private_full_path(); + #defined::__anchor_private_gen_idl_type() + .and_then(|ty| defined_types.insert(path, ty)); + });* + }; + (ret_ts, types_ts) + } + Err(()) => (quote! { None }, quote! {}), + }; + + let ident = &event_strct.ident; + let input_generics = &event_strct.generics; + let (impl_generics, ty_generics, where_clause) = input_generics.split_for_impl(); + + quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn __anchor_private_gen_idl_event( + defined_types: &mut std::collections::HashMap, + ) -> Option<#idl::IdlEvent> { + #types_ts + #ret_ts + } + } + } +} + +// generates the IDL generation impl for the Accounts struct +pub fn gen_idl_gen_impl_for_accounts_strct( + accs_strct: &AccountsStruct, + no_docs: bool, +) -> TokenStream { + let (idl, _) = get_module_paths(); + + let ident = &accs_strct.ident; + let (impl_generics, ty_generics, where_clause) = accs_strct.generics.split_for_impl(); + + let (accounts, acc_types): (Vec, Vec>) = accs_strct + .fields + .iter() + .map(|acc: &AccountField| match acc { + AccountField::CompositeField(comp_f) => { + let ty = if let syn::Type::Path(path) = &comp_f.raw_field.ty { + // some::path::Foo<'info> -> some::path::Foo + let mut res = syn::Path { + leading_colon: path.path.leading_colon, + segments: syn::punctuated::Punctuated::new(), + }; + for segment in &path.path.segments { + let s = syn::PathSegment { + ident: segment.ident.clone(), + arguments: syn::PathArguments::None, + }; + res.segments.push(s); + }; + res + } else { + panic!("expecting path") + }; + let name = comp_f.ident.to_string().to_mixed_case(); + (quote!{ + #idl::IdlAccountItem::IdlAccounts(#idl::IdlAccounts { + name: #name.into(), + accounts: #ty::__anchor_private_gen_idl_accounts(accounts, defined_types), + }) + }, None) + } + AccountField::Field(acc) => { + let name = acc.ident.to_string().to_mixed_case(); + let is_mut = acc.constraints.is_mutable(); + let is_signer = match acc.ty { + crate::Ty::Signer => true, + _ => acc.constraints.is_signer() + }; + let docs = match &acc.docs { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + let pda = quote!{None}; // TODO + + let acc_type_path = match &acc.ty { + crate::Ty::Account(ty) => Some(&ty.account_type_path), + crate::Ty::AccountLoader(ty) => Some(&ty.account_type_path), + _ => None, + }; + + (quote!{ + #idl::IdlAccountItem::IdlAccount(#idl::IdlAccount{ + name: #name.into(), + is_mut: #is_mut, + is_signer: #is_signer, + docs: #docs, + pda: #pda, + }) + }, acc_type_path) + } + }) + .unzip::, Vec, Vec>>(); + let acc_types = acc_types + .into_iter() + .flatten() + .collect::>(); + + quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::HashMap, + defined_types: &mut std::collections::HashMap, + ) -> Vec<#idl::IdlAccountItem> { + #({ + #acc_types::__anchor_private_insert_idl_defined(defined_types); + + let path = #acc_types::__anchor_private_full_path(); + #acc_types::__anchor_private_gen_idl_type() + .and_then(|ty| accounts.insert(path, ty)); + + });* + + vec![#(#accounts),*] + } + } + } +} + +// generates the IDL generation print function for the program module +pub fn gen_idl_print_function_for_program(program: &Program, no_docs: bool) -> TokenStream { + let (idl, serde_json) = get_module_paths(); + + let (instructions, defined) = program + .ixs + .iter() + .flat_map(|ix| -> Result<_, ()> { + let name = ix.ident.to_string().to_mixed_case(); + let docs = match &ix.docs { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + let ctx_ident = &ix.anchor_ident; + + let (args, mut defined) = ix + .args + .iter() + .map(|arg| { + let arg_name = arg.name.to_string().to_mixed_case(); + let docs = match docs::parse(&arg.raw_arg.attrs) { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + let (ty, defined) = idl_type_ts_from_syn_type(&arg.raw_arg.ty)?; + + Ok((quote! { + #idl::IdlField { + name: #arg_name.into(), + docs: #docs, + ty: #ty, + } + }, defined)) + }) + .collect::, ()>>()? + .into_iter() + .unzip::, Vec, Vec>>(); + + let returns = match idl_type_ts_from_syn_type(&ix.returns.ty) { + Ok((ty, def)) => { + defined.push(def); + quote!{ Some(#ty) } + }, + Err(()) => quote!{ None } + }; + + Ok((quote! { + #idl::IdlInstruction { + name: #name.into(), + docs: #docs, + accounts: #ctx_ident::__anchor_private_gen_idl_accounts( + &mut accounts, + &mut defined_types, + ), + args: vec![#(#args),*], + returns: #returns, + } + }, defined)) + }) + .unzip::>, Vec, Vec>>>(); + let defined = defined + .into_iter() + .flatten() + .flatten() + .collect::>(); + + let name = program.name.to_string(); + let docs = match &program.docs { + Some(docs) if !no_docs => quote! {Some(vec![#(#docs.into()),*])}, + _ => quote! {None}, + }; + + quote! { + #[test] + pub fn __anchor_private_print_idl_program() { + let mut accounts: std::collections::HashMap = + std::collections::HashMap::new(); + let mut defined_types: std::collections::HashMap = + std::collections::HashMap::new(); + + #({ + #defined::__anchor_private_insert_idl_defined(&mut defined_types); + + let path = #defined::__anchor_private_full_path(); + #defined::__anchor_private_gen_idl_type() + .and_then(|ty| defined_types.insert(path, ty)); + });* + + let instructions = vec![#(#instructions),*]; + + let idl = #idl::Idl { + version: env!("CARGO_PKG_VERSION").into(), + name: #name.into(), + docs: #docs, + constants: vec![], + instructions, + state: None, + accounts: accounts.into_values().collect(), + types: defined_types.into_values().collect(), + events: None, + errors: None, + metadata: None, + }; + + println!("---- IDL begin program ----"); + println!("{}", #serde_json::to_string_pretty(&idl).unwrap()); + println!("---- IDL end program ----"); + } + } +} + +pub fn gen_idl_print_function_for_event(event: &ItemStruct) -> TokenStream { + let (idl, serde_json) = get_module_paths(); + + let ident = &event.ident; + let fn_name = format_ident!("__anchor_private_print_idl_event_{}", ident.to_string()); + let impl_gen = gen_idl_gen_impl_for_event(event); + + quote! { + #impl_gen + + #[test] + pub fn #fn_name() { + let mut defined_types: std::collections::HashMap = std::collections::HashMap::new(); + let event = #ident::__anchor_private_gen_idl_event(&mut defined_types); + + if let Some(event) = event { + let json = #serde_json::json!({ + "event": event, + "defined_types": defined_types.into_values().collect::>() + }); + + println!("---- IDL begin event ----"); + println!("{}", #serde_json::to_string_pretty(&json).unwrap()); + println!("---- IDL end event ----"); + } + } + } +} + +pub fn gen_idl_print_function_for_constant(item: &syn::ItemConst) -> TokenStream { + let fn_name = format_ident!( + "__anchor_private_print_idl_const_{}", + item.ident.to_string() + ); + let (idl, serde_json) = get_module_paths(); + + let name = item.ident.to_string(); + let expr = &item.expr; + + let impl_ts = match idl_type_ts_from_syn_type(&item.ty) { + Ok((ty, _)) => quote! { + let value = format!("{}", #expr); + + let idl = #idl::IdlConst { + name: #name.into(), + ty: #ty, + value, + }; + + println!("---- IDL begin const ----"); + println!("{}", #serde_json::to_string_pretty(&idl).unwrap()); + println!("---- IDL end const ----"); + }, + Err(()) => quote! {}, + }; + + quote! { + #[test] + pub fn #fn_name() { + #impl_ts + } + } +} + +pub fn gen_idl_print_function_for_error(error: &Error) -> TokenStream { + let fn_name = format_ident!( + "__anchor_private_print_idl_error_{}", + error.ident.to_string() + ); + let (idl, serde_json) = get_module_paths(); + + let error_codes = error + .codes + .iter() + .map(|code| { + let id = code.id; + let name = code.ident.to_string(); + + let msg = match code.msg.clone() { + Some(msg) => quote! { Some(#msg.to_string()) }, + None => quote! { None }, + }; + + quote! { + #idl::IdlErrorCode { + code: anchor_lang::error::ERROR_CODE_OFFSET + #id, + name: #name.into(), + msg: #msg, + } + } + }) + .collect::>(); + + quote! { + #[test] + pub fn #fn_name() { + let errors = vec![#(#error_codes),*]; + + println!("---- IDL begin errors ----"); + println!("{}", #serde_json::to_string_pretty(&errors).unwrap()); + println!("---- IDL end errors ----"); + } + } +} diff --git a/lang/syn/src/idl/mod.rs b/lang/syn/src/idl/mod.rs index f77b7dfe5c..ec92c4db78 100644 --- a/lang/syn/src/idl/mod.rs +++ b/lang/syn/src/idl/mod.rs @@ -1,2 +1,6 @@ +#[cfg(feature = "idl-gen")] +pub mod gen; +#[cfg(feature = "idl-parse")] pub mod parse; +#[cfg(any(feature = "idl-types", feature = "idl-gen", feature = "idl-parse"))] pub mod types; diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index c589d87bfd..f89fa8d39c 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -22,7 +22,6 @@ pub mod codegen; pub mod hash; #[cfg(not(feature = "hash"))] pub(crate) mod hash; -#[cfg(feature = "idl")] pub mod idl; pub mod parser; diff --git a/tests/idl-generation/Anchor.toml b/tests/idl-generation/Anchor.toml new file mode 100644 index 0000000000..76a5904def --- /dev/null +++ b/tests/idl-generation/Anchor.toml @@ -0,0 +1,15 @@ +[features] +seeds = false +[programs.localnet] +idl = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" +idl_2 = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" + +[registry] +url = "https://anchor.projectserum.com" + +[provider] +cluster = "localnet" +wallet = "/home/work/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/tests/idl-generation/Cargo.toml b/tests/idl-generation/Cargo.toml new file mode 100644 index 0000000000..ef17a63c0a --- /dev/null +++ b/tests/idl-generation/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = [ + "programs/*" +] + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tests/idl-generation/gen_testdata.sh b/tests/idl-generation/gen_testdata.sh new file mode 100755 index 0000000000..860421f8f8 --- /dev/null +++ b/tests/idl-generation/gen_testdata.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd programs/idl +anchor idl parse --file src/lib.rs > ../../tests/testdata/idl_parse_exp.json +anchor idl build > ../../tests/testdata/idl_gen_exp.json \ No newline at end of file diff --git a/tests/idl-generation/package.json b/tests/idl-generation/package.json new file mode 100644 index 0000000000..167b106589 --- /dev/null +++ b/tests/idl-generation/package.json @@ -0,0 +1,19 @@ +{ + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@project-serum/anchor": "^0.24.2" + }, + "devDependencies": { + "chai": "^4.3.4", + "mocha": "^9.0.3", + "ts-mocha": "^8.0.0", + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "typescript": "^4.3.5", + "prettier": "^2.6.2" + } +} diff --git a/tests/idl-generation/programs/idl/Cargo.toml b/tests/idl-generation/programs/idl/Cargo.toml new file mode 100644 index 0000000000..f0af388311 --- /dev/null +++ b/tests/idl-generation/programs/idl/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "idl" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "idl" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +idl-gen = [ + "anchor-lang/idl-gen", + "some-external-program/idl-gen", +] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang" } +anchor-spl = { path = "../../../../spl" } +some-external-program = { path = "../some_external_program", features = ["no-entrypoint"] } diff --git a/tests/idl-generation/programs/idl/Xargo.toml b/tests/idl-generation/programs/idl/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/tests/idl-generation/programs/idl/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/idl-generation/programs/idl/src/lib.rs b/tests/idl-generation/programs/idl/src/lib.rs new file mode 100644 index 0000000000..7341eb044f --- /dev/null +++ b/tests/idl-generation/programs/idl/src/lib.rs @@ -0,0 +1,326 @@ +use anchor_lang::prelude::*; +use some_external_program; +use std::str::FromStr; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +#[constant] +pub const FOO_CONST: u128 = 1_000_000; +#[constant] +pub const BAR_CONST: u8 = 6; + +/// This is an example program used for testing +#[program] +pub mod example_program { + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> { + ctx.accounts.state.set_inner(State::default()); + Ok(()) + } + + /// Initializes an account with specified values + pub fn initialize_with_values( + ctx: Context, + bool_field: bool, + u8_field: u8, + i8_field: i8, + u16_field: u16, + i16_field: i16, + u32_field: u32, + i32_field: i32, + f32_field: f32, + u64_field: u64, + i64_field: i64, + f64_field: f64, + u128_field: u128, + i128_field: i128, + bytes_field: Vec, + string_field: String, + pubkey_field: Pubkey, + vec_field: Vec, + vec_struct_field: Vec, + option_field: Option, + option_struct_field: Option, + struct_field: FooStruct, + array_field: [bool; 3], + enum_field_1: FooEnum, + enum_field_2: FooEnum, + enum_field_3: FooEnum, + enum_field_4: FooEnum, + ) -> Result<()> { + ctx.accounts.state.set_inner(State { + bool_field, + u8_field, + i8_field, + u16_field, + i16_field, + u32_field, + i32_field, + f32_field, + u64_field, + i64_field, + f64_field, + u128_field, + i128_field, + bytes_field, + string_field, + pubkey_field, + vec_field, + vec_struct_field, + option_field, + option_struct_field, + struct_field, + array_field, + enum_field_1, + enum_field_2, + enum_field_3, + enum_field_4, + }); + + Ok(()) + } + + /// a separate instruction due to initialize_with_values having too many arguments + /// https://github.com/solana-labs/solana/issues/23978 + pub fn initialize_with_values2( + ctx: Context, + vec_of_option: Vec>, + box_field: Box, + ) -> Result { + ctx.accounts.state.set_inner(State2 { vec_of_option, box_field }); + Ok(SomeRetStruct { some_field: 3}) + } + + pub fn cause_error(_ctx: Context) -> Result<()> { + return Err(error!(ErrorCode::SomeError)); + } +} + +/// Enum type +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub enum FooEnum { + /// Tuple kind + Unnamed(bool, u8, BarStruct), + UnnamedSingle(BarStruct), + Named { + /// A bool field inside a struct tuple kind + bool_field: bool, + u8_field: u8, + nested: BarStruct, + }, + Struct(BarStruct), + OptionStruct(Option), + VecStruct(Vec), + NoFields, +} + +/// Bar struct type +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct BarStruct { + /// Some field + some_field: bool, + other_field: u8, +} + +impl Default for BarStruct { + fn default() -> Self { + return BarStruct { + some_field: true, + other_field: 10, + }; + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct FooStruct { + field1: u8, + field2: u16, + nested: BarStruct, + vec_nested: Vec, + option_nested: Option, + enum_field: FooEnum, +} + +impl Default for FooStruct { + fn default() -> Self { + return FooStruct { + field1: 123, + field2: 999, + nested: BarStruct::default(), + vec_nested: vec![BarStruct::default()], + option_nested: Some(BarStruct::default()), + enum_field: FooEnum::Named { + bool_field: true, + u8_field: 15, + nested: BarStruct::default(), + }, + }; + } +} + +/// An account containing various fields +#[account] +pub struct State { + /// A boolean field + bool_field: bool, + u8_field: u8, + i8_field: i8, + u16_field: u16, + i16_field: i16, + u32_field: u32, + i32_field: i32, + f32_field: f32, + u64_field: u64, + i64_field: i64, + f64_field: f64, + u128_field: u128, + i128_field: i128, + bytes_field: Vec, + string_field: String, + pubkey_field: Pubkey, + vec_field: Vec, + vec_struct_field: Vec, + option_field: Option, + option_struct_field: Option, + struct_field: FooStruct, + array_field: [bool; 3], + enum_field_1: FooEnum, + enum_field_2: FooEnum, + enum_field_3: FooEnum, + enum_field_4: FooEnum, +} + +impl Default for State { + fn default() -> Self { + // some arbitrary default values + return State { + bool_field: true, + u8_field: 234, + i8_field: -123, + u16_field: 62345, + i16_field: -31234, + u32_field: 1234567891, + i32_field: -1234567891, + f32_field: 123456.5, + u64_field: u64::MAX / 2 + 10, + i64_field: i64::MIN / 2 - 10, + f64_field: 1234567891.345, + u128_field: u128::MAX / 2 + 10, + i128_field: i128::MIN / 2 - 10, + bytes_field: vec![1, 2, 255, 254], + string_field: String::from("hello"), + pubkey_field: Pubkey::from_str("EPZP2wrcRtMxrAPJCXVEQaYD9eH7fH7h12YqKDcd4aS7").unwrap(), + vec_field: vec![1, 2, 100, 1000, u64::MAX], + vec_struct_field: vec![FooStruct::default()], + option_field: None, + option_struct_field: Some(FooStruct::default()), + struct_field: FooStruct::default(), + array_field: [true, false, true], + enum_field_1: FooEnum::Unnamed(false, 10, BarStruct::default()), + enum_field_2: FooEnum::Named { + bool_field: true, + u8_field: 20, + nested: BarStruct::default(), + }, + enum_field_3: FooEnum::Struct(BarStruct::default()), + enum_field_4: FooEnum::NoFields, + }; + } +} + +#[account] +pub struct State2 { + vec_of_option: Vec>, + box_field: Box, +} +impl Default for State2 { + fn default() -> Self { + return State2 { + vec_of_option: vec![None, Some(10)], + box_field: Box::new(true), + }; + } +} + +#[derive(Accounts)] +pub struct NestedAccounts<'info> { + /// Sysvar clock + clock: Sysvar<'info, Clock>, + rent: Sysvar<'info, Rent>, +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + /// State account + #[account( + init, + space = 8 + 1000, // TODO: use exact space required + payer = payer, + )] + state: Account<'info, State>, + + nested: NestedAccounts<'info>, + zc_account: AccountLoader<'info, SomeZcAccount>, + + #[account(mut)] + payer: Signer<'info>, + system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Initialize2<'info> { + #[account( + init, + space = 8 + 1000, // TODO: use exact space required + payer = payer, + )] + state: Account<'info, State2>, + + #[account(mut)] + payer: Signer<'info>, + system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct CauseError {} + +#[error_code] +pub enum ErrorCode { + #[msg("Example error.")] + SomeError, + #[msg("Another error.")] + OtherError, + ErrorWithoutMsg, +} + +mod some_other_module { + use super::*; + + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] + pub struct Baz { + some_u8: u8, + } +} + +#[event] +pub struct SomeEvent { + bool_field: bool, + external_baz: some_external_program::Baz, + other_module_baz: some_other_module::Baz, +} + +#[zero_copy] +pub struct ZcStruct { + pub some_field: u16, +} + +#[account(zero_copy)] +pub struct SomeZcAccount { + field: ZcStruct, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct SomeRetStruct { + pub some_field: u8, +} \ No newline at end of file diff --git a/tests/idl-generation/programs/some_external_program/Cargo.toml b/tests/idl-generation/programs/some_external_program/Cargo.toml new file mode 100644 index 0000000000..6814037fdc --- /dev/null +++ b/tests/idl-generation/programs/some_external_program/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "some-external-program" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "some_external_program" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-gen = ["anchor-lang/idl-gen"] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang" } +anchor-spl = { path = "../../../../spl" } + diff --git a/tests/idl-generation/programs/some_external_program/Xargo.toml b/tests/idl-generation/programs/some_external_program/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/tests/idl-generation/programs/some_external_program/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/idl-generation/programs/some_external_program/src/lib.rs b/tests/idl-generation/programs/some_external_program/src/lib.rs new file mode 100644 index 0000000000..0e620f8b5f --- /dev/null +++ b/tests/idl-generation/programs/some_external_program/src/lib.rs @@ -0,0 +1,20 @@ +use anchor_lang::prelude::*; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +#[program] +pub mod idl_2 { + use super::*; + + pub fn initialize(_ctx: Context, _baz: Baz) -> Result<()> { + Ok(()) + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct Baz { + some_field: u8, +} + +#[derive(Accounts)] +pub struct Initialize {} diff --git a/tests/idl-generation/test.sh b/tests/idl-generation/test.sh new file mode 100755 index 0000000000..0f1267c878 --- /dev/null +++ b/tests/idl-generation/test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -x +set -e + +TMPDIR=$(mktemp -d) + +cd programs/idl +anchor idl parse --file src/lib.rs > $TMPDIR/idl_parse_act.json +anchor idl build > $TMPDIR/idl_gen_act.json + +cd ../.. +echo "----------------------------------------------------" +echo "idl parse before > after" +echo "----------------------------------------------------" +echo "" +diff -y --color tests/testdata/idl_parse_exp.json $TMPDIR/idl_parse_act.json +PARSE_RETCODE=$? + +echo "" +echo "" +echo "----------------------------------------------------" +echo "idl build before > after" +echo "----------------------------------------------------" +echo "" +diff -y --color tests/testdata/idl_gen_exp.json $TMPDIR/idl_gen_act.json +GEN_RETCODE=$? + +# returns 0 when ok, 1 or 2 when outputs differ +exit $((PARSE_RETCODE+GEN_RETCODE)) diff --git a/tests/idl-generation/tests/testdata/idl_gen_exp.json b/tests/idl-generation/tests/testdata/idl_gen_exp.json new file mode 100644 index 0000000000..629b2185d0 --- /dev/null +++ b/tests/idl-generation/tests/testdata/idl_gen_exp.json @@ -0,0 +1,727 @@ +{ + "version": "0.1.0", + "name": "example_program", + "docs": [ + "This is an example program used for testing" + ], + "constants": [ + { + "name": "BAR_CONST", + "type": "u8", + "value": "6" + }, + { + "name": "FOO_CONST", + "type": "u128", + "value": "1000000" + } + ], + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": true, + "docs": [ + "State account" + ] + }, + { + "name": "nested", + "accounts": [ + { + "name": "clock", + "isMut": false, + "isSigner": false, + "docs": [ + "Sysvar clock" + ] + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "zcAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeWithValues", + "docs": [ + "Initializes an account with specified values" + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": true, + "docs": [ + "State account" + ] + }, + { + "name": "nested", + "accounts": [ + { + "name": "clock", + "isMut": false, + "isSigner": false, + "docs": [ + "Sysvar clock" + ] + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "zcAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "boolField", + "type": "bool" + }, + { + "name": "u8Field", + "type": "u8" + }, + { + "name": "i8Field", + "type": "i8" + }, + { + "name": "u16Field", + "type": "u16" + }, + { + "name": "i16Field", + "type": "i16" + }, + { + "name": "u32Field", + "type": "u32" + }, + { + "name": "i32Field", + "type": "i32" + }, + { + "name": "f32Field", + "type": "f32" + }, + { + "name": "u64Field", + "type": "u64" + }, + { + "name": "i64Field", + "type": "i64" + }, + { + "name": "f64Field", + "type": "f64" + }, + { + "name": "u128Field", + "type": "u128" + }, + { + "name": "i128Field", + "type": "i128" + }, + { + "name": "bytesField", + "type": "bytes" + }, + { + "name": "stringField", + "type": "string" + }, + { + "name": "pubkeyField", + "type": "publicKey" + }, + { + "name": "vecField", + "type": { + "vec": "u64" + } + }, + { + "name": "vecStructField", + "type": { + "vec": { + "defined": "idl::FooStruct" + } + } + }, + { + "name": "optionField", + "type": { + "option": "bool" + } + }, + { + "name": "optionStructField", + "type": { + "option": { + "defined": "idl::FooStruct" + } + } + }, + { + "name": "structField", + "type": { + "defined": "idl::FooStruct" + } + }, + { + "name": "arrayField", + "type": { + "array": [ + "bool", + 3 + ] + } + }, + { + "name": "enumField1", + "type": { + "defined": "idl::FooEnum" + } + }, + { + "name": "enumField2", + "type": { + "defined": "idl::FooEnum" + } + }, + { + "name": "enumField3", + "type": { + "defined": "idl::FooEnum" + } + }, + { + "name": "enumField4", + "type": { + "defined": "idl::FooEnum" + } + } + ] + }, + { + "name": "initializeWithValues2", + "docs": [ + "a separate instruction due to initialize_with_values having too many arguments", + "https://github.com/solana-labs/solana/issues/23978" + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "vecOfOption", + "type": { + "vec": { + "option": "u64" + } + } + }, + { + "name": "boxField", + "type": "bool" + } + ], + "returns": { + "defined": "idl::SomeRetStruct" + } + }, + { + "name": "causeError", + "accounts": [], + "args": [] + } + ], + "accounts": [ + { + "name": "SomeZcAccount", + "full_path": "idl::SomeZcAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "field", + "type": { + "defined": "idl::ZcStruct" + } + } + ] + } + }, + { + "name": "State", + "full_path": "idl::State", + "docs": [ + "An account containing various fields" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "boolField", + "docs": [ + "A boolean field" + ], + "type": "bool" + }, + { + "name": "u8Field", + "type": "u8" + }, + { + "name": "i8Field", + "type": "i8" + }, + { + "name": "u16Field", + "type": "u16" + }, + { + "name": "i16Field", + "type": "i16" + }, + { + "name": "u32Field", + "type": "u32" + }, + { + "name": "i32Field", + "type": "i32" + }, + { + "name": "f32Field", + "type": "f32" + }, + { + "name": "u64Field", + "type": "u64" + }, + { + "name": "i64Field", + "type": "i64" + }, + { + "name": "f64Field", + "type": "f64" + }, + { + "name": "u128Field", + "type": "u128" + }, + { + "name": "i128Field", + "type": "i128" + }, + { + "name": "bytesField", + "type": "bytes" + }, + { + "name": "stringField", + "type": "string" + }, + { + "name": "pubkeyField", + "type": "publicKey" + }, + { + "name": "vecField", + "type": { + "vec": "u64" + } + }, + { + "name": "vecStructField", + "type": { + "vec": { + "defined": "idl::FooStruct" + } + } + }, + { + "name": "optionField", + "type": { + "option": "bool" + } + }, + { + "name": "optionStructField", + "type": { + "option": { + "defined": "idl::FooStruct" + } + } + }, + { + "name": "structField", + "type": { + "defined": "idl::FooStruct" + } + }, + { + "name": "arrayField", + "type": { + "array": [ + "bool", + 3 + ] + } + }, + { + "name": "enumField1", + "type": { + "defined": "idl::FooEnum" + } + }, + { + "name": "enumField2", + "type": { + "defined": "idl::FooEnum" + } + }, + { + "name": "enumField3", + "type": { + "defined": "idl::FooEnum" + } + }, + { + "name": "enumField4", + "type": { + "defined": "idl::FooEnum" + } + } + ] + } + }, + { + "name": "State2", + "full_path": "idl::State2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vecOfOption", + "type": { + "vec": { + "option": "u64" + } + } + }, + { + "name": "boxField", + "type": "bool" + } + ] + } + } + ], + "types": [ + { + "name": "BarStruct", + "full_path": "idl::BarStruct", + "docs": [ + "Bar struct type" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "docs": [ + "Some field" + ], + "type": "bool" + }, + { + "name": "otherField", + "type": "u8" + } + ] + } + }, + { + "name": "FooEnum", + "full_path": "idl::FooEnum", + "docs": [ + "Enum type" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Unnamed", + "fields": [ + "bool", + "u8", + { + "defined": "idl::BarStruct" + } + ] + }, + { + "name": "UnnamedSingle", + "fields": [ + { + "defined": "idl::BarStruct" + } + ] + }, + { + "name": "Named", + "fields": [ + { + "name": "boolField", + "docs": [ + "A bool field inside a struct tuple kind" + ], + "type": "bool" + }, + { + "name": "u8Field", + "type": "u8" + }, + { + "name": "nested", + "type": { + "defined": "idl::BarStruct" + } + } + ] + }, + { + "name": "Struct", + "fields": [ + { + "defined": "idl::BarStruct" + } + ] + }, + { + "name": "OptionStruct", + "fields": [ + { + "option": { + "defined": "idl::BarStruct" + } + } + ] + }, + { + "name": "VecStruct", + "fields": [ + { + "vec": { + "defined": "idl::BarStruct" + } + } + ] + }, + { + "name": "NoFields" + } + ] + } + }, + { + "name": "FooStruct", + "full_path": "idl::FooStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "field1", + "type": "u8" + }, + { + "name": "field2", + "type": "u16" + }, + { + "name": "nested", + "type": { + "defined": "idl::BarStruct" + } + }, + { + "name": "vecNested", + "type": { + "vec": { + "defined": "idl::BarStruct" + } + } + }, + { + "name": "optionNested", + "type": { + "option": { + "defined": "idl::BarStruct" + } + } + }, + { + "name": "enumField", + "type": { + "defined": "idl::FooEnum" + } + } + ] + } + }, + { + "name": "SomeRetStruct", + "full_path": "idl::SomeRetStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "type": "u8" + } + ] + } + }, + { + "name": "ZcStruct", + "full_path": "idl::ZcStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "type": "u16" + } + ] + } + }, + { + "name": "Baz", + "full_path": "idl::some_other_module::Baz", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someU8", + "type": "u8" + } + ] + } + }, + { + "name": "Baz", + "full_path": "some_external_program::Baz", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "type": "u8" + } + ] + } + } + ], + "events": [ + { + "name": "SomeEvent", + "fields": [ + { + "name": "boolField", + "type": "bool", + "index": false + }, + { + "name": "externalBaz", + "type": { + "defined": "some_external_program::Baz" + }, + "index": false + }, + { + "name": "otherModuleBaz", + "type": { + "defined": "idl::some_other_module::Baz" + }, + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "SomeError", + "msg": "Example error." + }, + { + "code": 6001, + "name": "OtherError", + "msg": "Another error." + }, + { + "code": 6002, + "name": "ErrorWithoutMsg" + } + ] +} diff --git a/tests/idl-generation/tests/testdata/idl_parse_exp.json b/tests/idl-generation/tests/testdata/idl_parse_exp.json new file mode 100644 index 0000000000..899969853d --- /dev/null +++ b/tests/idl-generation/tests/testdata/idl_parse_exp.json @@ -0,0 +1,705 @@ +{ + "version": "0.1.0", + "name": "example_program", + "docs": [ + "This is an example program used for testing" + ], + "constants": [ + { + "name": "FOO_CONST", + "type": "u128", + "value": "1_000_000" + }, + { + "name": "BAR_CONST", + "type": "u8", + "value": "6" + } + ], + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": true, + "docs": [ + "State account" + ] + }, + { + "name": "nested", + "accounts": [ + { + "name": "clock", + "isMut": false, + "isSigner": false, + "docs": [ + "Sysvar clock" + ] + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "zcAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeWithValues", + "docs": [ + "Initializes an account with specified values" + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": true, + "docs": [ + "State account" + ] + }, + { + "name": "nested", + "accounts": [ + { + "name": "clock", + "isMut": false, + "isSigner": false, + "docs": [ + "Sysvar clock" + ] + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "zcAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "boolField", + "type": "bool" + }, + { + "name": "u8Field", + "type": "u8" + }, + { + "name": "i8Field", + "type": "i8" + }, + { + "name": "u16Field", + "type": "u16" + }, + { + "name": "i16Field", + "type": "i16" + }, + { + "name": "u32Field", + "type": "u32" + }, + { + "name": "i32Field", + "type": "i32" + }, + { + "name": "f32Field", + "type": "f32" + }, + { + "name": "u64Field", + "type": "u64" + }, + { + "name": "i64Field", + "type": "i64" + }, + { + "name": "f64Field", + "type": "f64" + }, + { + "name": "u128Field", + "type": "u128" + }, + { + "name": "i128Field", + "type": "i128" + }, + { + "name": "bytesField", + "type": "bytes" + }, + { + "name": "stringField", + "type": "string" + }, + { + "name": "pubkeyField", + "type": "publicKey" + }, + { + "name": "vecField", + "type": { + "vec": "u64" + } + }, + { + "name": "vecStructField", + "type": { + "vec": { + "defined": "FooStruct" + } + } + }, + { + "name": "optionField", + "type": { + "option": "bool" + } + }, + { + "name": "optionStructField", + "type": { + "option": { + "defined": "FooStruct" + } + } + }, + { + "name": "structField", + "type": { + "defined": "FooStruct" + } + }, + { + "name": "arrayField", + "type": { + "array": [ + "bool", + 3 + ] + } + }, + { + "name": "enumField1", + "type": { + "defined": "FooEnum" + } + }, + { + "name": "enumField2", + "type": { + "defined": "FooEnum" + } + }, + { + "name": "enumField3", + "type": { + "defined": "FooEnum" + } + }, + { + "name": "enumField4", + "type": { + "defined": "FooEnum" + } + } + ] + }, + { + "name": "initializeWithValues2", + "docs": [ + "a separate instruction due to initialize_with_values having too many arguments", + "https://github.com/solana-labs/solana/issues/23978" + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "vecOfOption", + "type": { + "vec": { + "option": "u64" + } + } + }, + { + "name": "boxField", + "type": "bool" + } + ], + "returns": { + "defined": "SomeRetStruct" + } + }, + { + "name": "causeError", + "accounts": [], + "args": [] + } + ], + "accounts": [ + { + "name": "State", + "docs": [ + "An account containing various fields" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "boolField", + "docs": [ + "A boolean field" + ], + "type": "bool" + }, + { + "name": "u8Field", + "type": "u8" + }, + { + "name": "i8Field", + "type": "i8" + }, + { + "name": "u16Field", + "type": "u16" + }, + { + "name": "i16Field", + "type": "i16" + }, + { + "name": "u32Field", + "type": "u32" + }, + { + "name": "i32Field", + "type": "i32" + }, + { + "name": "f32Field", + "type": "f32" + }, + { + "name": "u64Field", + "type": "u64" + }, + { + "name": "i64Field", + "type": "i64" + }, + { + "name": "f64Field", + "type": "f64" + }, + { + "name": "u128Field", + "type": "u128" + }, + { + "name": "i128Field", + "type": "i128" + }, + { + "name": "bytesField", + "type": "bytes" + }, + { + "name": "stringField", + "type": "string" + }, + { + "name": "pubkeyField", + "type": "publicKey" + }, + { + "name": "vecField", + "type": { + "vec": "u64" + } + }, + { + "name": "vecStructField", + "type": { + "vec": { + "defined": "FooStruct" + } + } + }, + { + "name": "optionField", + "type": { + "option": "bool" + } + }, + { + "name": "optionStructField", + "type": { + "option": { + "defined": "FooStruct" + } + } + }, + { + "name": "structField", + "type": { + "defined": "FooStruct" + } + }, + { + "name": "arrayField", + "type": { + "array": [ + "bool", + 3 + ] + } + }, + { + "name": "enumField1", + "type": { + "defined": "FooEnum" + } + }, + { + "name": "enumField2", + "type": { + "defined": "FooEnum" + } + }, + { + "name": "enumField3", + "type": { + "defined": "FooEnum" + } + }, + { + "name": "enumField4", + "type": { + "defined": "FooEnum" + } + } + ] + } + }, + { + "name": "State2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vecOfOption", + "type": { + "vec": { + "option": "u64" + } + } + }, + { + "name": "boxField", + "type": "bool" + } + ] + } + }, + { + "name": "SomeZcAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "field", + "type": { + "defined": "ZcStruct" + } + } + ] + } + } + ], + "types": [ + { + "name": "Baz", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someU8", + "type": "u8" + } + ] + } + }, + { + "name": "BarStruct", + "docs": [ + "Bar struct type" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "docs": [ + "Some field" + ], + "type": "bool" + }, + { + "name": "otherField", + "type": "u8" + } + ] + } + }, + { + "name": "FooStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "field1", + "type": "u8" + }, + { + "name": "field2", + "type": "u16" + }, + { + "name": "nested", + "type": { + "defined": "BarStruct" + } + }, + { + "name": "vecNested", + "type": { + "vec": { + "defined": "BarStruct" + } + } + }, + { + "name": "optionNested", + "type": { + "option": { + "defined": "BarStruct" + } + } + }, + { + "name": "enumField", + "type": { + "defined": "FooEnum" + } + } + ] + } + }, + { + "name": "ZcStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "type": "u16" + } + ] + } + }, + { + "name": "SomeRetStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "someField", + "type": "u8" + } + ] + } + }, + { + "name": "FooEnum", + "docs": [ + "Enum type" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Unnamed", + "fields": [ + "bool", + "u8", + { + "defined": "BarStruct" + } + ] + }, + { + "name": "UnnamedSingle", + "fields": [ + { + "defined": "BarStruct" + } + ] + }, + { + "name": "Named", + "fields": [ + { + "name": "bool_field", + "docs": [ + "A bool field inside a struct tuple kind" + ], + "type": "bool" + }, + { + "name": "u8_field", + "type": "u8" + }, + { + "name": "nested", + "type": { + "defined": "BarStruct" + } + } + ] + }, + { + "name": "Struct", + "fields": [ + { + "defined": "BarStruct" + } + ] + }, + { + "name": "OptionStruct", + "fields": [ + { + "option": { + "defined": "BarStruct" + } + } + ] + }, + { + "name": "VecStruct", + "fields": [ + { + "vec": { + "defined": "BarStruct" + } + } + ] + }, + { + "name": "NoFields" + } + ] + } + } + ], + "events": [ + { + "name": "SomeEvent", + "fields": [ + { + "name": "boolField", + "type": "bool", + "index": false + }, + { + "name": "externalBaz", + "type": { + "defined": "some_external_program::Baz" + }, + "index": false + }, + { + "name": "otherModuleBaz", + "type": { + "defined": "some_other_module::Baz" + }, + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "SomeError", + "msg": "Example error." + }, + { + "code": 6001, + "name": "OtherError", + "msg": "Another error." + }, + { + "code": 6002, + "name": "ErrorWithoutMsg" + } + ] +} diff --git a/tests/idl-generation/tsconfig.json b/tests/idl-generation/tsconfig.json new file mode 100644 index 0000000000..cd5d2e3d06 --- /dev/null +++ b/tests/idl-generation/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} diff --git a/tests/package.json b/tests/package.json index 35112548c6..142b787ae4 100644 --- a/tests/package.json +++ b/tests/package.json @@ -16,6 +16,7 @@ "escrow", "events", "floats", + "idl-generation", "ido-pool", "interface", "lockup", diff --git a/tests/yarn.lock b/tests/yarn.lock index a5da001806..11cfca60bc 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -66,6 +66,7 @@ js-sha256 "^0.9.0" pako "^2.0.3" snake-case "^3.0.4" + superstruct "^0.15.4" toml "^3.0.0" "@project-serum/borsh@^0.2.2": @@ -341,6 +342,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -448,6 +456,21 @@ chokidar@3.5.2: optionalDependencies: fsevents "~2.3.2" +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + circular-json@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" @@ -510,6 +533,13 @@ debug@4.3.2: dependencies: ms "2.1.2" +debug@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decamelize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" @@ -666,6 +696,18 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -874,6 +916,13 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -886,6 +935,34 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" + integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mocha@^9.1.3: version "9.1.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.3.tgz#8a623be6b323810493d8c8f6f7667440fa469fdb" @@ -931,6 +1008,11 @@ nanoid@3.1.25: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -1136,6 +1218,11 @@ superstruct@^0.14.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + supports-color@8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" @@ -1187,6 +1274,15 @@ traverse-chain@~0.1.0: resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE= +ts-mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.0.0.tgz#41a8d099ac90dbbc64b06976c5025ffaebc53cb9" + integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + ts-mocha@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-8.0.0.tgz#962d0fa12eeb6468aa1a6b594bb3bbc818da3ef0" @@ -1287,6 +1383,11 @@ workerpool@6.1.5: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"