Add docs field to idl (#1561)
This commit is contained in:
parent
0916361f5e
commit
ed15922f1a
|
@ -15,6 +15,7 @@ The minor version will be incremented upon a breaking change and the patch versi
|
|||
* cli: Add `--program-keypair` to `anchor deploy` ([#1786](https://github.com/project-serum/anchor/pull/1786)).
|
||||
* spl: Add more derived traits to `TokenAccount` to `Mint` ([#1818](https://github.com/project-serum/anchor/pull/1818)).
|
||||
* cli: Add compilation optimizations to cli template ([#1807](https://github.com/project-serum/anchor/pull/1807)).
|
||||
* cli: `build` now adds docs to idl. This can be turned off with `--no-docs` ([#1561](https://github.com/project-serum/anchor/pull/1561)).
|
||||
* lang: Add `PartialEq` and `Eq` for `anchor_lang::Error` ([#1544](https://github.com/project-serum/anchor/pull/1544)).
|
||||
|
||||
### Fixes
|
||||
|
|
|
@ -183,6 +183,7 @@ impl WithPath<Config> {
|
|||
version,
|
||||
self.features.seeds,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
r.push(Program {
|
||||
lib_name,
|
||||
|
|
|
@ -106,6 +106,9 @@ pub enum Command {
|
|||
last = true
|
||||
)]
|
||||
cargo_args: Vec<String>,
|
||||
/// Suppress doc strings in IDL output
|
||||
#[clap(long)]
|
||||
no_docs: bool,
|
||||
},
|
||||
/// Expands macros (wrapper around cargo expand)
|
||||
///
|
||||
|
@ -353,6 +356,9 @@ pub enum IdlCommand {
|
|||
/// Output file for the TypeScript IDL.
|
||||
#[clap(short = 't', long)]
|
||||
out_ts: Option<String>,
|
||||
/// Suppress doc strings in output
|
||||
#[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.
|
||||
|
@ -388,6 +394,7 @@ pub fn entry(opts: Opts) -> Result<()> {
|
|||
bootstrap,
|
||||
cargo_args,
|
||||
skip_lint,
|
||||
no_docs,
|
||||
} => build(
|
||||
&opts.cfg_override,
|
||||
idl,
|
||||
|
@ -401,6 +408,7 @@ pub fn entry(opts: Opts) -> Result<()> {
|
|||
None,
|
||||
None,
|
||||
cargo_args,
|
||||
no_docs,
|
||||
),
|
||||
Command::Verify {
|
||||
program_id,
|
||||
|
@ -748,6 +756,7 @@ pub fn build(
|
|||
stdout: Option<File>, // Used for the package registry server.
|
||||
stderr: Option<File>, // Used for the package registry server.
|
||||
cargo_args: Vec<String>,
|
||||
no_docs: bool,
|
||||
) -> Result<()> {
|
||||
// Change to the workspace member directory, if needed.
|
||||
if let Some(program_name) = program_name.as_ref() {
|
||||
|
@ -793,6 +802,7 @@ pub fn build(
|
|||
stderr,
|
||||
cargo_args,
|
||||
skip_lint,
|
||||
no_docs,
|
||||
)?,
|
||||
// If the Cargo.toml is at the root, build the entire workspace.
|
||||
Some(cargo) if cargo.path().parent() == cfg.path().parent() => build_all(
|
||||
|
@ -805,6 +815,7 @@ pub fn build(
|
|||
stderr,
|
||||
cargo_args,
|
||||
skip_lint,
|
||||
no_docs,
|
||||
)?,
|
||||
// Cargo.toml represents a single package. Build it.
|
||||
Some(cargo) => build_cwd(
|
||||
|
@ -817,6 +828,7 @@ pub fn build(
|
|||
stderr,
|
||||
cargo_args,
|
||||
skip_lint,
|
||||
no_docs,
|
||||
)?,
|
||||
}
|
||||
|
||||
|
@ -836,6 +848,7 @@ fn build_all(
|
|||
stderr: Option<File>, // Used for the package registry server.
|
||||
cargo_args: Vec<String>,
|
||||
skip_lint: bool,
|
||||
no_docs: bool,
|
||||
) -> Result<()> {
|
||||
let cur_dir = std::env::current_dir()?;
|
||||
let r = match cfg_path.parent() {
|
||||
|
@ -852,6 +865,7 @@ fn build_all(
|
|||
stderr.as_ref().map(|f| f.try_clone()).transpose()?,
|
||||
cargo_args.clone(),
|
||||
skip_lint,
|
||||
no_docs,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
|
@ -873,6 +887,7 @@ fn build_cwd(
|
|||
stderr: Option<File>,
|
||||
cargo_args: Vec<String>,
|
||||
skip_lint: bool,
|
||||
no_docs: bool,
|
||||
) -> Result<()> {
|
||||
match cargo_toml.parent() {
|
||||
None => return Err(anyhow!("Unable to find parent")),
|
||||
|
@ -888,12 +903,14 @@ fn build_cwd(
|
|||
stderr,
|
||||
skip_lint,
|
||||
cargo_args,
|
||||
no_docs,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Builds an anchor program in a docker image and copies the build artifacts
|
||||
// into the `target/` directory.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_cwd_verifiable(
|
||||
cfg: &WithPath<Config>,
|
||||
cargo_toml: PathBuf,
|
||||
|
@ -902,6 +919,7 @@ fn build_cwd_verifiable(
|
|||
stderr: Option<File>,
|
||||
skip_lint: bool,
|
||||
cargo_args: Vec<String>,
|
||||
no_docs: bool,
|
||||
) -> Result<()> {
|
||||
// Create output dirs.
|
||||
let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
|
||||
|
@ -932,7 +950,7 @@ fn build_cwd_verifiable(
|
|||
Ok(_) => {
|
||||
// Build the idl.
|
||||
println!("Extracting the IDL");
|
||||
if let Ok(Some(idl)) = extract_idl(cfg, "src/lib.rs", skip_lint) {
|
||||
if let Ok(Some(idl)) = extract_idl(cfg, "src/lib.rs", skip_lint, no_docs) {
|
||||
// Write out the JSON file.
|
||||
println!("Writing the IDL file");
|
||||
let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name));
|
||||
|
@ -1207,7 +1225,7 @@ fn _build_cwd(
|
|||
}
|
||||
|
||||
// Always assume idl is located at src/lib.rs.
|
||||
if let Some(idl) = extract_idl(cfg, "src/lib.rs", skip_lint)? {
|
||||
if let Some(idl) = extract_idl(cfg, "src/lib.rs", skip_lint, false)? {
|
||||
// JSON out path.
|
||||
let out = match idl_out {
|
||||
None => PathBuf::from(".").join(&idl.name).with_extension("json"),
|
||||
|
@ -1272,6 +1290,7 @@ fn verify(
|
|||
None, // stdout
|
||||
None, // stderr
|
||||
cargo_args,
|
||||
false,
|
||||
)?;
|
||||
std::env::set_current_dir(&cur_dir)?;
|
||||
|
||||
|
@ -1292,7 +1311,7 @@ fn verify(
|
|||
}
|
||||
|
||||
// Verify IDL (only if it's not a buffer account).
|
||||
if let Some(local_idl) = extract_idl(&cfg, "src/lib.rs", true)? {
|
||||
if let Some(local_idl) = extract_idl(&cfg, "src/lib.rs", true, false)? {
|
||||
if bin_ver.state != BinVerificationState::Buffer {
|
||||
let deployed_idl = fetch_idl(cfg_override, program_id)?;
|
||||
if local_idl != deployed_idl {
|
||||
|
@ -1469,12 +1488,23 @@ fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<Idl> {
|
|||
serde_json::from_slice(&s[..]).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn extract_idl(cfg: &WithPath<Config>, file: &str, skip_lint: bool) -> Result<Option<Idl>> {
|
||||
fn extract_idl(
|
||||
cfg: &WithPath<Config>,
|
||||
file: &str,
|
||||
skip_lint: bool,
|
||||
no_docs: bool,
|
||||
) -> Result<Option<Idl>> {
|
||||
let file = shellexpand::tilde(file);
|
||||
let manifest_from_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap());
|
||||
let cargo = Manifest::discover_from_path(manifest_from_path)?
|
||||
.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
|
||||
anchor_syn::idl::file::parse(&*file, cargo.version(), cfg.features.seeds, !skip_lint)
|
||||
anchor_syn::idl::file::parse(
|
||||
&*file,
|
||||
cargo.version(),
|
||||
cfg.features.seeds,
|
||||
no_docs,
|
||||
!skip_lint,
|
||||
)
|
||||
}
|
||||
|
||||
fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
|
||||
|
@ -1501,7 +1531,12 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
|
|||
} => idl_set_authority(cfg_override, program_id, address, new_authority),
|
||||
IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id),
|
||||
IdlCommand::Authority { program_id } => idl_authority(cfg_override, program_id),
|
||||
IdlCommand::Parse { file, out, out_ts } => idl_parse(cfg_override, file, out, out_ts),
|
||||
IdlCommand::Parse {
|
||||
file,
|
||||
out,
|
||||
out_ts,
|
||||
no_docs,
|
||||
} => idl_parse(cfg_override, file, out, out_ts, no_docs),
|
||||
IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out),
|
||||
}
|
||||
}
|
||||
|
@ -1763,9 +1798,10 @@ fn idl_parse(
|
|||
file: String,
|
||||
out: Option<String>,
|
||||
out_ts: Option<String>,
|
||||
no_docs: bool,
|
||||
) -> Result<()> {
|
||||
let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
|
||||
let idl = extract_idl(&cfg, &file, true)?.ok_or_else(|| anyhow!("IDL not parsed"))?;
|
||||
let idl = extract_idl(&cfg, &file, true, no_docs)?.ok_or_else(|| anyhow!("IDL not parsed"))?;
|
||||
let out = match out {
|
||||
None => OutFile::Stdout,
|
||||
Some(out) => OutFile::File(PathBuf::from(out)),
|
||||
|
@ -1832,6 +1868,7 @@ fn test(
|
|||
None,
|
||||
None,
|
||||
cargo_args,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
|
||||
|
@ -2930,6 +2967,7 @@ fn publish(
|
|||
None,
|
||||
None,
|
||||
cargo_args,
|
||||
true,
|
||||
)?;
|
||||
|
||||
// Success. Now we can finally upload to the server without worrying
|
||||
|
@ -3029,6 +3067,7 @@ fn localnet(
|
|||
None,
|
||||
None,
|
||||
cargo_args,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
.map(|f: &AccountField| match f {
|
||||
AccountField::CompositeField(s) => {
|
||||
let name = &s.ident;
|
||||
let docs = if !s.docs.is_empty() {
|
||||
proc_macro2::TokenStream::from_str(&format!("#[doc = r#\"{}\"#]", s.docs))
|
||||
.unwrap()
|
||||
let docs = if let Some(ref docs) = s.docs {
|
||||
docs.iter()
|
||||
.map(|docs_line| {
|
||||
proc_macro2::TokenStream::from_str(&format!(
|
||||
"#[doc = r#\"{}\"#]",
|
||||
docs_line
|
||||
))
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
@ -41,9 +48,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
}
|
||||
AccountField::Field(f) => {
|
||||
let name = &f.ident;
|
||||
let docs = if !f.docs.is_empty() {
|
||||
proc_macro2::TokenStream::from_str(&format!("#[doc = r#\"{}\"#]", f.docs))
|
||||
.unwrap()
|
||||
let docs = if let Some(ref docs) = f.docs {
|
||||
docs.iter()
|
||||
.map(|docs_line| {
|
||||
proc_macro2::TokenStream::from_str(&format!(
|
||||
"#[doc = r#\"{}\"#]",
|
||||
docs_line
|
||||
))
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
|
|
@ -22,9 +22,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
.map(|f: &AccountField| match f {
|
||||
AccountField::CompositeField(s) => {
|
||||
let name = &s.ident;
|
||||
let docs = if !s.docs.is_empty() {
|
||||
proc_macro2::TokenStream::from_str(&format!("#[doc = r#\"{}\"#]", s.docs))
|
||||
.unwrap()
|
||||
let docs = if let Some(ref docs) = s.docs {
|
||||
docs.iter()
|
||||
.map(|docs_line| {
|
||||
proc_macro2::TokenStream::from_str(&format!(
|
||||
"#[doc = r#\"{}\"#]",
|
||||
docs_line
|
||||
))
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
@ -42,9 +49,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
}
|
||||
AccountField::Field(f) => {
|
||||
let name = &f.ident;
|
||||
let docs = if !f.docs.is_empty() {
|
||||
proc_macro2::TokenStream::from_str(&format!("#[doc = r#\"{}\"#]", f.docs))
|
||||
.unwrap()
|
||||
let docs = if let Some(ref docs) = f.docs {
|
||||
docs.iter()
|
||||
.map(|docs_line| {
|
||||
proc_macro2::TokenStream::from_str(&format!(
|
||||
"#[doc = r#\"{}\"#]",
|
||||
docs_line
|
||||
))
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::idl::*;
|
||||
use crate::parser::context::CrateContext;
|
||||
use crate::parser::{self, accounts, error, program};
|
||||
use crate::parser::{self, accounts, docs, error, program};
|
||||
use crate::Ty;
|
||||
use crate::{AccountField, AccountsStruct, StateIx};
|
||||
use anyhow::Result;
|
||||
|
@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet};
|
|||
use std::path::Path;
|
||||
|
||||
const DERIVE_NAME: &str = "Accounts";
|
||||
// TODO: sharee this with `anchor_lang` crate.
|
||||
// TODO: share this with `anchor_lang` crate.
|
||||
const ERROR_CODE_OFFSET: u32 = 6000;
|
||||
|
||||
// Parse an entire interface file.
|
||||
|
@ -18,6 +18,7 @@ pub fn parse(
|
|||
filename: impl AsRef<Path>,
|
||||
version: String,
|
||||
seeds_feature: bool,
|
||||
no_docs: bool,
|
||||
safety_checks: bool,
|
||||
) -> Result<Option<Idl>> {
|
||||
let ctx = CrateContext::parse(filename)?;
|
||||
|
@ -29,7 +30,14 @@ pub fn parse(
|
|||
None => return Ok(None),
|
||||
Some(m) => m,
|
||||
};
|
||||
let p = program::parse(program_mod)?;
|
||||
let mut p = program::parse(program_mod)?;
|
||||
|
||||
if no_docs {
|
||||
p.docs = None;
|
||||
for ix in &mut p.ixs {
|
||||
ix.docs = None;
|
||||
}
|
||||
}
|
||||
|
||||
let accs = parse_account_derives(&ctx);
|
||||
|
||||
|
@ -51,19 +59,31 @@ pub fn parse(
|
|||
.map(|arg| {
|
||||
let mut tts = proc_macro2::TokenStream::new();
|
||||
arg.raw_arg.ty.to_tokens(&mut tts);
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&arg.raw_arg.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ty = tts.to_string().parse().unwrap();
|
||||
IdlField {
|
||||
name: arg.name.to_string().to_mixed_case(),
|
||||
docs: doc,
|
||||
ty,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let accounts_strct =
|
||||
accs.get(&method.anchor_ident.to_string()).unwrap();
|
||||
let accounts =
|
||||
idl_accounts(&ctx, accounts_strct, &accs, seeds_feature);
|
||||
let accounts = idl_accounts(
|
||||
&ctx,
|
||||
accounts_strct,
|
||||
&accs,
|
||||
seeds_feature,
|
||||
no_docs,
|
||||
);
|
||||
IdlInstruction {
|
||||
name,
|
||||
docs: None,
|
||||
accounts,
|
||||
args,
|
||||
returns: None,
|
||||
|
@ -91,9 +111,15 @@ pub fn parse(
|
|||
syn::FnArg::Typed(arg_typed) => {
|
||||
let mut tts = proc_macro2::TokenStream::new();
|
||||
arg_typed.ty.to_tokens(&mut tts);
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&arg_typed.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ty = tts.to_string().parse().unwrap();
|
||||
IdlField {
|
||||
name: parser::tts_to_string(&arg_typed.pat).to_mixed_case(),
|
||||
docs: doc,
|
||||
ty,
|
||||
}
|
||||
}
|
||||
|
@ -101,9 +127,11 @@ pub fn parse(
|
|||
})
|
||||
.collect();
|
||||
let accounts_strct = accs.get(&anchor_ident.to_string()).unwrap();
|
||||
let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature);
|
||||
let accounts =
|
||||
idl_accounts(&ctx, accounts_strct, &accs, seeds_feature, no_docs);
|
||||
IdlInstruction {
|
||||
name,
|
||||
docs: None,
|
||||
accounts,
|
||||
args,
|
||||
returns: None,
|
||||
|
@ -120,9 +148,15 @@ pub fn parse(
|
|||
.map(|f: &syn::Field| {
|
||||
let mut tts = proc_macro2::TokenStream::new();
|
||||
f.ty.to_tokens(&mut tts);
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&f.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ty = tts.to_string().parse().unwrap();
|
||||
IdlField {
|
||||
name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
|
||||
docs: doc,
|
||||
ty,
|
||||
}
|
||||
})
|
||||
|
@ -131,6 +165,7 @@ pub fn parse(
|
|||
};
|
||||
IdlTypeDefinition {
|
||||
name: state.name,
|
||||
docs: None,
|
||||
ty: IdlTypeDefinitionTy::Struct { fields },
|
||||
}
|
||||
};
|
||||
|
@ -158,14 +193,22 @@ pub fn parse(
|
|||
let args = ix
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| IdlField {
|
||||
name: arg.name.to_string().to_mixed_case(),
|
||||
ty: to_idl_type(&ctx, &arg.raw_arg.ty),
|
||||
.map(|arg| {
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&arg.raw_arg.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
IdlField {
|
||||
name: arg.name.to_string().to_mixed_case(),
|
||||
docs: doc,
|
||||
ty: to_idl_type(&ctx, &arg.raw_arg.ty),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// todo: don't unwrap
|
||||
let accounts_strct = accs.get(&ix.anchor_ident.to_string()).unwrap();
|
||||
let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature);
|
||||
let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature, no_docs);
|
||||
let ret_type_str = ix.returns.ty.to_token_stream().to_string();
|
||||
let returns = match ret_type_str.as_str() {
|
||||
"()" => None,
|
||||
|
@ -173,6 +216,7 @@ pub fn parse(
|
|||
};
|
||||
IdlInstruction {
|
||||
name: ix.ident.to_string().to_mixed_case(),
|
||||
docs: ix.docs.clone(),
|
||||
accounts,
|
||||
args,
|
||||
returns,
|
||||
|
@ -213,7 +257,7 @@ pub fn parse(
|
|||
// All user defined types.
|
||||
let mut accounts = vec![];
|
||||
let mut types = vec![];
|
||||
let ty_defs = parse_ty_defs(&ctx)?;
|
||||
let ty_defs = parse_ty_defs(&ctx, no_docs)?;
|
||||
|
||||
let account_structs = parse_accounts(&ctx);
|
||||
let account_names: HashSet<String> = account_structs
|
||||
|
@ -247,6 +291,7 @@ pub fn parse(
|
|||
Ok(Some(Idl {
|
||||
version,
|
||||
name: p.name.to_string(),
|
||||
docs: p.docs.clone(),
|
||||
state,
|
||||
instructions,
|
||||
types,
|
||||
|
@ -380,7 +425,7 @@ fn parse_consts(ctx: &CrateContext) -> Vec<&syn::ItemConst> {
|
|||
}
|
||||
|
||||
// Parse all user defined types in the file.
|
||||
fn parse_ty_defs(ctx: &CrateContext) -> Result<Vec<IdlTypeDefinition>> {
|
||||
fn parse_ty_defs(ctx: &CrateContext, no_docs: bool) -> Result<Vec<IdlTypeDefinition>> {
|
||||
ctx.structs()
|
||||
.filter_map(|item_strct| {
|
||||
// Only take serializable types
|
||||
|
@ -407,13 +452,24 @@ fn parse_ty_defs(ctx: &CrateContext) -> Result<Vec<IdlTypeDefinition>> {
|
|||
}
|
||||
|
||||
let name = item_strct.ident.to_string();
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&item_strct.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fields = match &item_strct.fields {
|
||||
syn::Fields::Named(fields) => fields
|
||||
.named
|
||||
.iter()
|
||||
.map(|f: &syn::Field| {
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&f.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(IdlField {
|
||||
name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
|
||||
docs: doc,
|
||||
ty: to_idl_type(ctx, &f.ty),
|
||||
})
|
||||
})
|
||||
|
@ -424,11 +480,17 @@ fn parse_ty_defs(ctx: &CrateContext) -> Result<Vec<IdlTypeDefinition>> {
|
|||
|
||||
Some(fields.map(|fields| IdlTypeDefinition {
|
||||
name,
|
||||
docs: doc,
|
||||
ty: IdlTypeDefinitionTy::Struct { fields },
|
||||
}))
|
||||
})
|
||||
.chain(ctx.enums().map(|enm| {
|
||||
let name = enm.ident.to_string();
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&enm.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let variants = enm
|
||||
.variants
|
||||
.iter()
|
||||
|
@ -450,8 +512,17 @@ fn parse_ty_defs(ctx: &CrateContext) -> Result<Vec<IdlTypeDefinition>> {
|
|||
.iter()
|
||||
.map(|f: &syn::Field| {
|
||||
let name = f.ident.as_ref().unwrap().to_string();
|
||||
let doc = if !no_docs {
|
||||
docs::parse(&f.attrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ty = to_idl_type(ctx, &f.ty);
|
||||
IdlField { name, ty }
|
||||
IdlField {
|
||||
name,
|
||||
docs: doc,
|
||||
ty,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Some(EnumFields::Named(fields))
|
||||
|
@ -462,6 +533,7 @@ fn parse_ty_defs(ctx: &CrateContext) -> Result<Vec<IdlTypeDefinition>> {
|
|||
.collect::<Vec<IdlEnumVariant>>();
|
||||
Ok(IdlTypeDefinition {
|
||||
name,
|
||||
docs: doc,
|
||||
ty: IdlTypeDefinitionTy::Enum { variants },
|
||||
})
|
||||
}))
|
||||
|
@ -541,6 +613,7 @@ fn idl_accounts(
|
|||
accounts: &AccountsStruct,
|
||||
global_accs: &HashMap<String, AccountsStruct>,
|
||||
seeds_feature: bool,
|
||||
no_docs: bool,
|
||||
) -> Vec<IdlAccountItem> {
|
||||
accounts
|
||||
.fields
|
||||
|
@ -550,7 +623,7 @@ fn idl_accounts(
|
|||
let accs_strct = global_accs.get(&comp_f.symbol).unwrap_or_else(|| {
|
||||
panic!("Could not resolve Accounts symbol {}", comp_f.symbol)
|
||||
});
|
||||
let accounts = idl_accounts(ctx, accs_strct, global_accs, seeds_feature);
|
||||
let accounts = idl_accounts(ctx, accs_strct, global_accs, seeds_feature, no_docs);
|
||||
IdlAccountItem::IdlAccounts(IdlAccounts {
|
||||
name: comp_f.ident.to_string().to_mixed_case(),
|
||||
accounts,
|
||||
|
@ -563,6 +636,7 @@ fn idl_accounts(
|
|||
Ty::Signer => true,
|
||||
_ => acc.constraints.is_signer(),
|
||||
},
|
||||
docs: if !no_docs { acc.docs.clone() } else { None },
|
||||
pda: pda::parse(ctx, accounts, acc, seeds_feature),
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -8,6 +8,8 @@ pub mod pda;
|
|||
pub struct Idl {
|
||||
pub version: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub docs: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub constants: Vec<IdlConst>,
|
||||
pub instructions: Vec<IdlInstruction>,
|
||||
|
@ -43,6 +45,8 @@ pub struct IdlState {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct IdlInstruction {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub docs: Option<Vec<String>>,
|
||||
pub accounts: Vec<IdlAccountItem>,
|
||||
pub args: Vec<IdlField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -69,6 +73,8 @@ pub struct IdlAccount {
|
|||
pub name: String,
|
||||
pub is_mut: bool,
|
||||
pub is_signer: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub docs: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub pda: Option<IdlPda>,
|
||||
}
|
||||
|
@ -120,6 +126,8 @@ pub struct IdlSeedConst {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct IdlField {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub docs: Option<Vec<String>>,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: IdlType,
|
||||
}
|
||||
|
@ -141,6 +149,8 @@ pub struct IdlEventField {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct IdlTypeDefinition {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub docs: Option<Vec<String>>,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: IdlTypeDefinitionTy,
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ pub struct Program {
|
|||
pub state: Option<State>,
|
||||
pub ixs: Vec<Ix>,
|
||||
pub name: Ident,
|
||||
pub docs: Option<Vec<String>>,
|
||||
pub program_mod: ItemMod,
|
||||
pub fallback_fn: Option<FallbackFn>,
|
||||
}
|
||||
|
@ -84,6 +85,7 @@ pub struct StateInterface {
|
|||
pub struct Ix {
|
||||
pub raw_method: ItemFn,
|
||||
pub ident: Ident,
|
||||
pub docs: Option<Vec<String>>,
|
||||
pub args: Vec<IxArg>,
|
||||
pub returns: IxReturn,
|
||||
// The ident for the struct deriving Accounts.
|
||||
|
@ -93,6 +95,7 @@ pub struct Ix {
|
|||
#[derive(Debug)]
|
||||
pub struct IxArg {
|
||||
pub name: Ident,
|
||||
pub docs: Option<Vec<String>>,
|
||||
pub raw_arg: PatType,
|
||||
}
|
||||
|
||||
|
@ -213,8 +216,8 @@ pub struct Field {
|
|||
pub constraints: ConstraintGroup,
|
||||
pub instruction_constraints: ConstraintGroup,
|
||||
pub ty: Ty,
|
||||
/// Documentation string.
|
||||
pub docs: String,
|
||||
/// IDL Doc comment
|
||||
pub docs: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
|
@ -494,8 +497,8 @@ pub struct CompositeField {
|
|||
pub instruction_constraints: ConstraintGroup,
|
||||
pub symbol: String,
|
||||
pub raw_field: syn::Field,
|
||||
/// Documentation string.
|
||||
pub docs: String,
|
||||
/// IDL Doc comment
|
||||
pub docs: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// A type of an account field.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::parser::docs;
|
||||
use crate::*;
|
||||
use syn::parse::{Error as ParseError, Result as ParseResult};
|
||||
use syn::punctuated::Punctuated;
|
||||
|
@ -145,21 +146,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
|
||||
pub fn parse_account_field(f: &syn::Field, has_instruction_api: bool) -> ParseResult<AccountField> {
|
||||
let ident = f.ident.clone().unwrap();
|
||||
let docs: String = f
|
||||
.attrs
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let meta_result = a.parse_meta();
|
||||
if let Ok(syn::Meta::NameValue(meta)) = meta_result {
|
||||
if meta.path.is_ident("doc") {
|
||||
if let syn::Lit::Str(doc) = meta.lit {
|
||||
return format!(" {}\n", doc.value().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_string()
|
||||
})
|
||||
.collect::<String>();
|
||||
let docs = docs::parse(&f.attrs);
|
||||
let account_field = match is_field_primitive(f)? {
|
||||
true => {
|
||||
let ty = parse_ty(f)?;
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
use syn::{Lit::Str, Meta::NameValue};
|
||||
|
||||
// returns vec of doc strings
|
||||
pub fn parse(attrs: &[syn::Attribute]) -> Option<Vec<String>> {
|
||||
let doc_strings: Vec<String> = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| match attr.parse_meta() {
|
||||
Ok(NameValue(meta)) => {
|
||||
if meta.path.is_ident("doc") {
|
||||
if let Str(doc) = meta.lit {
|
||||
let val = doc.value().trim().to_string();
|
||||
if val.starts_with("CHECK:") {
|
||||
return None;
|
||||
}
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if doc_strings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(doc_strings)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod accounts;
|
||||
pub mod context;
|
||||
pub mod docs;
|
||||
pub mod error;
|
||||
pub mod program;
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::parser::docs;
|
||||
use crate::parser::program::ctx_accounts_ident;
|
||||
use crate::{FallbackFn, Ix, IxArg, IxReturn};
|
||||
use syn::parse::{Error as ParseError, Result as ParseResult};
|
||||
|
@ -23,11 +24,13 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<(Vec<Ix>, Option<Fallbac
|
|||
})
|
||||
.map(|method: &syn::ItemFn| {
|
||||
let (ctx, args) = parse_args(method)?;
|
||||
let docs = docs::parse(&method.attrs);
|
||||
let returns = parse_return(method)?;
|
||||
let anchor_ident = ctx_accounts_ident(&ctx.raw_arg)?;
|
||||
Ok(Ix {
|
||||
raw_method: method.clone(),
|
||||
ident: method.sig.ident.clone(),
|
||||
docs,
|
||||
args,
|
||||
anchor_ident,
|
||||
returns,
|
||||
|
@ -72,12 +75,14 @@ pub fn parse_args(method: &syn::ItemFn) -> ParseResult<(IxArg, Vec<IxArg>)> {
|
|||
.iter()
|
||||
.map(|arg: &syn::FnArg| match arg {
|
||||
syn::FnArg::Typed(arg) => {
|
||||
let docs = docs::parse(&arg.attrs);
|
||||
let ident = match &*arg.pat {
|
||||
syn::Pat::Ident(ident) => &ident.ident,
|
||||
_ => return Err(ParseError::new(arg.pat.span(), "expected argument name")),
|
||||
};
|
||||
Ok(IxArg {
|
||||
name: ident.clone(),
|
||||
docs,
|
||||
raw_arg: arg.clone(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::parser::docs;
|
||||
use crate::Program;
|
||||
use syn::parse::{Error as ParseError, Result as ParseResult};
|
||||
use syn::spanned::Spanned;
|
||||
|
@ -7,11 +8,13 @@ mod state;
|
|||
|
||||
pub fn parse(program_mod: syn::ItemMod) -> ParseResult<Program> {
|
||||
let state = state::parse(&program_mod)?;
|
||||
let docs = docs::parse(&program_mod.attrs);
|
||||
let (ixs, fallback_fn) = instructions::parse(&program_mod)?;
|
||||
Ok(Program {
|
||||
state,
|
||||
ixs,
|
||||
name: program_mod.ident.clone(),
|
||||
docs,
|
||||
program_mod,
|
||||
fallback_fn,
|
||||
})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::parser;
|
||||
use crate::parser::docs;
|
||||
use crate::parser::program::ctx_accounts_ident;
|
||||
use crate::{IxArg, State, StateInterface, StateIx};
|
||||
use syn::parse::{Error as ParseError, Result as ParseResult};
|
||||
|
@ -175,6 +176,7 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<Option<State>> {
|
|||
syn::FnArg::Typed(arg) => Some(arg),
|
||||
})
|
||||
.map(|raw_arg| {
|
||||
let docs = docs::parse(&raw_arg.attrs);
|
||||
let ident = match &*raw_arg.pat {
|
||||
syn::Pat::Ident(ident) => &ident.ident,
|
||||
_ => {
|
||||
|
@ -186,6 +188,7 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<Option<State>> {
|
|||
};
|
||||
Ok(IxArg {
|
||||
name: ident.clone(),
|
||||
docs,
|
||||
raw_arg: raw_arg.clone(),
|
||||
})
|
||||
})
|
||||
|
@ -256,12 +259,14 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<Option<State>> {
|
|||
syn::FnArg::Typed(arg) => Some(arg),
|
||||
})
|
||||
.map(|raw_arg| {
|
||||
let docs = docs::parse(&raw_arg.attrs);
|
||||
let ident = match &*raw_arg.pat {
|
||||
syn::Pat::Ident(ident) => &ident.ident,
|
||||
_ => panic!("invalid syntax"),
|
||||
};
|
||||
IxArg {
|
||||
name: ident.clone(),
|
||||
docs,
|
||||
raw_arg: raw_arg.clone(),
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ wallet = "~/.config/solana/id.json"
|
|||
[programs.localnet]
|
||||
misc = "3TEqcc8xhrhdspwbvoamUJe2borm4Nr72JxL66k6rgrh"
|
||||
misc2 = "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"
|
||||
idl_doc = "BqmKjZGVa8fqyWuojJzG16zaKSV1GjAisZToNuvEaz6m"
|
||||
init_if_needed = "BZoppwWi6jMnydnUBEJzotgEXHwLr3b3NramJgZtWeF2"
|
||||
|
||||
[workspace]
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "idl_doc"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
rust-version = "1.56"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "idl_doc"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,34 @@
|
|||
//! Testing the extraction of doc comments from the IDL.
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
|
||||
declare_id!("BqmKjZGVa8fqyWuojJzG16zaKSV1GjAisZToNuvEaz6m");
|
||||
|
||||
/// This is a doc comment for the program
|
||||
#[program]
|
||||
pub mod idl_doc {
|
||||
use super::*;
|
||||
|
||||
/// This instruction doc should appear in the IDL
|
||||
pub fn test_idl_doc_parse(
|
||||
_ctx: Context<TestIdlDocParse>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom account doc comment should appear in the IDL
|
||||
#[account]
|
||||
pub struct DataWithDoc {
|
||||
/// Account attribute doc comment should appear in the IDL
|
||||
pub data: u16,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestIdlDocParse<'info> {
|
||||
/// This account doc comment should appear in the IDL
|
||||
/// This is a multi-line comment
|
||||
pub act: Account<'info, DataWithDoc>,
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
[scripts]
|
||||
test = "yarn run ts-mocha -t 1000000 ./tests/idl_doc/*.ts"
|
|
@ -0,0 +1,50 @@
|
|||
import * as anchor from "@project-serum/anchor";
|
||||
import { Program, Wallet } from "@project-serum/anchor";
|
||||
import { IdlDoc } from "../../target/types/idl_doc";
|
||||
const { expect } = require("chai");
|
||||
const idl_doc_idl = require("../../target/idl/idl_doc.json");
|
||||
|
||||
describe("idl_doc", () => {
|
||||
// Configure the client to use the local cluster.
|
||||
const provider = anchor.AnchorProvider.env();
|
||||
const wallet = provider.wallet as Wallet;
|
||||
anchor.setProvider(provider);
|
||||
const program = anchor.workspace.IdlDoc as Program<IdlDoc>;
|
||||
|
||||
describe("IDL doc strings", () => {
|
||||
const instruction = program.idl.instructions.find(
|
||||
(i) => i.name === "testIdlDocParse"
|
||||
);
|
||||
it("includes instruction doc comment", async () => {
|
||||
expect(instruction.docs).to.have.same.members([
|
||||
"This instruction doc should appear in the IDL",
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes account doc comment", async () => {
|
||||
const act = instruction.accounts.find((i) => i.name === "act");
|
||||
expect(act.docs).to.have.same.members([
|
||||
"This account doc comment should appear in the IDL",
|
||||
"This is a multi-line comment",
|
||||
]);
|
||||
});
|
||||
|
||||
const dataWithDoc = program.idl.accounts.find(
|
||||
// @ts-expect-error
|
||||
(i) => i.name === "DataWithDoc"
|
||||
);
|
||||
|
||||
it("includes accounts doc comment", async () => {
|
||||
expect(dataWithDoc.docs).to.have.same.members([
|
||||
"Custom account doc comment should appear in the IDL",
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes account attribute doc comment", async () => {
|
||||
const dataField = dataWithDoc.type.fields.find((i) => i.name === "data");
|
||||
expect(dataField.docs).to.have.same.members([
|
||||
"Account attribute doc comment should appear in the IDL",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@ import * as borsh from "@project-serum/borsh";
|
|||
export type Idl = {
|
||||
version: string;
|
||||
name: string;
|
||||
docs?: string[];
|
||||
instructions: IdlInstruction[];
|
||||
state?: IdlState;
|
||||
accounts?: IdlAccountDef[];
|
||||
|
@ -36,6 +37,7 @@ export type IdlEventField = {
|
|||
|
||||
export type IdlInstruction = {
|
||||
name: string;
|
||||
docs?: string[];
|
||||
accounts: IdlAccountItem[];
|
||||
args: IdlField[];
|
||||
returns?: IdlType;
|
||||
|
@ -54,6 +56,7 @@ export type IdlAccount = {
|
|||
name: string;
|
||||
isMut: boolean;
|
||||
isSigner: boolean;
|
||||
docs?: string[];
|
||||
pda?: IdlPda;
|
||||
};
|
||||
|
||||
|
@ -67,11 +70,13 @@ export type IdlSeed = any; // TODO
|
|||
// A nested/recursive version of IdlAccount.
|
||||
export type IdlAccounts = {
|
||||
name: string;
|
||||
docs?: string[];
|
||||
accounts: IdlAccountItem[];
|
||||
};
|
||||
|
||||
export type IdlField = {
|
||||
name: string;
|
||||
docs?: string[];
|
||||
type: IdlType;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue