Add docs field to idl (#1561)

This commit is contained in:
ebrightfield 2022-04-21 14:37:41 -06:00 committed by GitHub
parent 0916361f5e
commit ed15922f1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 350 additions and 52 deletions

View File

@ -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

View File

@ -183,6 +183,7 @@ impl WithPath<Config> {
version,
self.features.seeds,
false,
false,
)?;
r.push(Program {
lib_name,

View File

@ -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,
)?;
}

View File

@ -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!()
};

View File

@ -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!()
};

View File

@ -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),
}),
})

View File

@ -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,
}

View File

@ -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.

View File

@ -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)?;

View File

@ -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)
}
}

View File

@ -1,5 +1,6 @@
pub mod accounts;
pub mod context;
pub mod docs;
pub mod error;
pub mod program;

View File

@ -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(),
})
}

View File

@ -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,
})

View File

@ -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(),
}
})

View File

@ -5,6 +5,7 @@ wallet = "~/.config/solana/id.json"
[programs.localnet]
misc = "3TEqcc8xhrhdspwbvoamUJe2borm4Nr72JxL66k6rgrh"
misc2 = "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"
idl_doc = "BqmKjZGVa8fqyWuojJzG16zaKSV1GjAisZToNuvEaz6m"
init_if_needed = "BZoppwWi6jMnydnUBEJzotgEXHwLr3b3NramJgZtWeF2"
[workspace]

View File

@ -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"] }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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>,
}

View File

@ -0,0 +1,2 @@
[scripts]
test = "yarn run ts-mocha -t 1000000 ./tests/idl_doc/*.ts"

View File

@ -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",
]);
});
});
});

View File

@ -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;
};