Browse Source

Add docs field to idl (#1561)

ebrightfield 3 years ago
parent
commit
ed15922f1a

+ 1 - 0
CHANGELOG.md

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

+ 1 - 0
cli/src/config.rs

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

+ 46 - 7
cli/src/lib.rs

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

+ 20 - 6
lang/syn/src/codegen/accounts/__client_accounts.rs

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

+ 20 - 6
lang/syn/src/codegen/accounts/__cpi_client_accounts.rs

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

+ 88 - 14
lang/syn/src/idl/file.rs

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

+ 10 - 0
lang/syn/src/idl/mod.rs

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

+ 7 - 4
lang/syn/src/lib.rs

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

+ 2 - 15
lang/syn/src/parser/accounts/mod.rs

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

+ 28 - 0
lang/syn/src/parser/docs.rs

@@ -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 - 0
lang/syn/src/parser/mod.rs

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

+ 5 - 0
lang/syn/src/parser/program/instructions.rs

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

+ 3 - 0
lang/syn/src/parser/program/mod.rs

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

+ 5 - 0
lang/syn/src/parser/program/state.rs

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

+ 1 - 0
tests/misc/Anchor.toml

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

+ 19 - 0
tests/misc/programs/idl_doc/Cargo.toml

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

+ 2 - 0
tests/misc/programs/idl_doc/Xargo.toml

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

+ 34 - 0
tests/misc/programs/idl_doc/src/lib.rs

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

+ 2 - 0
tests/misc/tests/idl_doc/Test.toml

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

+ 50 - 0
tests/misc/tests/idl_doc/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 - 0
ts/src/idl.ts

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