From 521108e5db2778cf0bf240c624c811b990ffe0a6 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 23 Feb 2023 18:46:47 +0200 Subject: [PATCH 1/9] frame/proc: Helpers to parse pallet documentation attributes Signed-off-by: Alexandru Vasile --- .../src/pallet/expand/documentation.rs | 130 ++++++++++++++++++ .../procedural/src/pallet/expand/mod.rs | 1 + 2 files changed, 131 insertions(+) create mode 100644 frame/support/procedural/src/pallet/expand/documentation.rs diff --git a/frame/support/procedural/src/pallet/expand/documentation.rs b/frame/support/procedural/src/pallet/expand/documentation.rs new file mode 100644 index 0000000000000..309342cc7da17 --- /dev/null +++ b/frame/support/procedural/src/pallet/expand/documentation.rs @@ -0,0 +1,130 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::pallet::Def; +use proc_macro2::{Ident, TokenStream}; +use quote::ToTokens; +use syn::{ + parenthesized, + parse::{self, Parse, ParseStream}, + spanned::Spanned, + Attribute, Lit, Token, +}; + +const DOC: &'static str = "doc"; +const INCLUDE_STR: &'static str = "include_str"; +const PALLET_DOC: &'static str = "pallet_doc"; + +/// Get the documentation file path from the `pallet_doc` attribute. +/// +/// Supported format: +/// `#[pallet_doc(PATH)]`: The path of the file from which the documentation is loaded +fn parse_pallet_doc_value(attr: &Attribute) -> syn::Result { + let span = attr.span(); + + let meta = attr.parse_meta()?; + let syn::Meta::List(metalist) = meta else { + let msg = + "The `pallet_doc` attribute must receive arguments as a list. Supported format: `pallet_doc(PATH)`"; + return Err(syn::Error::new(span, msg)) + }; + + let paths: Vec<_> = metalist + .nested + .into_iter() + .map(|nested| { + let syn::NestedMeta::Lit(lit) = nested else { + let msg = "The `pallet_doc` received an unsupported argument. Supported format: `pallet_doc(PATH)`"; + return Err(syn::Error::new(span, msg)) + }; + + Ok(lit) + }) + .collect::>()?; + + if paths.len() != 1 { + let msg = "The `pallet_doc` attribute must receive only one argument. Supported format: `pallet_doc(PATH)`"; + return Err(syn::Error::new(span, msg)) + } + + Ok(DocMetaValue::Path(paths[0].clone())) +} + +/// Get the value from the `doc` comment attribute: +/// +/// Supported formats: +/// - `#[doc = "A doc string"]`: Documentation as a string literal +/// - `#[doc = include_str!(PATH)]`: Documentation obtained from a path +fn parse_doc_value(attr: &Attribute) -> Option { + let Some(ident) = attr.path.get_ident() else { + return None + }; + if ident != DOC { + return None + } + + let parser = |input: ParseStream| DocMetaValue::parse(input); + parse::Parser::parse2(parser, attr.tokens.clone()).ok() +} + +/// Supported documentation tokens. +#[derive(Debug)] +enum DocMetaValue { + /// Documentation with string literals. + /// + /// `#[doc = "Lit"]` + Lit(Lit), + /// Documentation with `include_str!` macro. + /// + /// The string literal represents the file `PATH`. + /// + /// `#[doc = include_str!(PATH)]` + Path(Lit), +} + +impl Parse for DocMetaValue { + fn parse(input: ParseStream) -> syn::Result { + let _token: Token![=] = input.parse()?; + + if input.peek(Lit) { + return input.parse().map(DocMetaValue::Lit) + } + + let ident: Ident = input.parse()?; + if ident != INCLUDE_STR { + return Err(input.error("expected include_str ident")) + } + let _token: Token![!] = input.parse()?; + + // We must have a path literal inside `(...)` + let content; + parenthesized!(content in input); + content.parse().map(DocMetaValue::Path) + } +} + +impl ToTokens for DocMetaValue { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + DocMetaValue::Lit(lit) => lit.to_tokens(tokens), + DocMetaValue::Path(path_lit) => { + let decl = quote::quote!(include_str!(#path_lit)); + tokens.extend(decl) + }, + } + } +} diff --git a/frame/support/procedural/src/pallet/expand/mod.rs b/frame/support/procedural/src/pallet/expand/mod.rs index 3ccbf91b4f130..c3c69c3e241c7 100644 --- a/frame/support/procedural/src/pallet/expand/mod.rs +++ b/frame/support/procedural/src/pallet/expand/mod.rs @@ -18,6 +18,7 @@ mod call; mod config; mod constants; +mod documentation; mod error; mod event; mod genesis_build; From 6915a655d4a086e1b4c4d1b4161bb54a75249dbb Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 23 Feb 2023 18:49:06 +0200 Subject: [PATCH 2/9] frame/proc: Expand pallet with runtime metadata documentation Signed-off-by: Alexandru Vasile --- .../src/pallet/expand/documentation.rs | 84 +++++++++++++++++++ .../procedural/src/pallet/expand/mod.rs | 3 + 2 files changed, 87 insertions(+) diff --git a/frame/support/procedural/src/pallet/expand/documentation.rs b/frame/support/procedural/src/pallet/expand/documentation.rs index 309342cc7da17..5ba4195fbb08f 100644 --- a/frame/support/procedural/src/pallet/expand/documentation.rs +++ b/frame/support/procedural/src/pallet/expand/documentation.rs @@ -128,3 +128,87 @@ impl ToTokens for DocMetaValue { } } } + +/// Extract the documentation from the given pallet definition +/// to include in the runtime metadata. +/// +/// Implement a `pallet_documentation_metadata` function to fetch the +/// documentation that is included in the metadata. +/// +/// The documentation is placed at the top of the module similar to: +/// +/// ```ignore +/// #[pallet] +/// /// Documentation for pallet +/// #[doc = "Documentation for pallet"] +/// #[doc = include_str!("../README.md")] +/// #[pallet_doc("../documentation1.md")] +/// #[pallet_doc("../documentation1.md")] +/// pub mod pallet {} +/// ``` +/// +/// # pallet_doc +/// +/// The `pallet_doc` attribute accepts only one argument: the path to a file +/// containing the documentation that should be included in the metadata. +/// +/// Argument is similar to `include_str`. +/// +/// The documentation parsed by this attribute is not expanded on the pallet +/// definition. In contract, #[doc = include_str!("../README.md")] is expanded +/// on the pallet definition as well as included in the metadata. +pub fn expand_documentation(def: &mut Def) -> proc_macro2::TokenStream { + let frame_support = &def.frame_support; + let type_impl_gen = &def.type_impl_generics(proc_macro2::Span::call_site()); + let type_use_gen = &def.type_use_generics(proc_macro2::Span::call_site()); + let pallet_ident = &def.pallet_struct.pallet; + let where_clauses = &def.config.where_clause; + + // TODO: Use [drain_filter](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter) when it is stable. + + // The `pallet_doc` attributes are excluded from the generation of the pallet, + // but they are included in the runtime metadata. + let mut pallet_docs = Vec::with_capacity(def.item.attrs.len()); + let mut index = 0; + while index < def.item.attrs.len() { + let attr = &def.item.attrs[index]; + if let Some(ident) = attr.path.get_ident() { + if ident == PALLET_DOC { + let elem = def.item.attrs.remove(index); + pallet_docs.push(elem); + // Do not increment the index, we have just removed the + // element from the attributes. + continue + } + } + + index += 1; + } + + // Capture the `#[doc = include_str!("../README.md")]` and `#[doc = "Documentation"]`. + let mut docs: Vec<_> = def.item.attrs.iter().filter_map(parse_doc_value).collect(); + + // Capture the `#[pallet_doc("../README.md")]`. + let pallet_docs: Vec<_> = match pallet_docs + .into_iter() + .map(|attr| parse_pallet_doc_value(&attr)) + .collect::>() + { + Ok(docs) => docs, + Err(err) => return err.into_compile_error(), + }; + + docs.extend(pallet_docs); + + quote::quote!( + impl<#type_impl_gen> #pallet_ident<#type_use_gen> #where_clauses{ + + #[doc(hidden)] + pub fn pallet_documentation_metadata() + -> #frame_support::sp_std::vec::Vec<&'static str> + { + #frame_support::sp_std::vec![ #( #docs ),* ] + } + } + ) +} diff --git a/frame/support/procedural/src/pallet/expand/mod.rs b/frame/support/procedural/src/pallet/expand/mod.rs index c3c69c3e241c7..09a25a7101ee7 100644 --- a/frame/support/procedural/src/pallet/expand/mod.rs +++ b/frame/support/procedural/src/pallet/expand/mod.rs @@ -53,6 +53,8 @@ pub fn merge_where_clauses(clauses: &[&Option]) -> Option proc_macro2::TokenStream { + // Remove the `pallet_doc` attribute first. + let metadata_docs = documentation::expand_documentation(&mut def); let constants = constants::expand_constants(&mut def); let pallet_struct = pallet_struct::expand_pallet_struct(&mut def); let config = config::expand_config(&mut def); @@ -83,6 +85,7 @@ pub fn expand(mut def: Def) -> proc_macro2::TokenStream { } let new_items = quote::quote!( + #metadata_docs #constants #pallet_struct #config From 771bdd7d3053f1c7893d64b720d6ebc2a5151452 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 23 Feb 2023 18:54:45 +0200 Subject: [PATCH 3/9] frame/dispatch: Implement doc function getter for dispatch Signed-off-by: Alexandru Vasile --- .../src/pallet/expand/documentation.rs | 2 +- frame/support/src/dispatch.rs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/frame/support/procedural/src/pallet/expand/documentation.rs b/frame/support/procedural/src/pallet/expand/documentation.rs index 5ba4195fbb08f..131ef0042ba32 100644 --- a/frame/support/procedural/src/pallet/expand/documentation.rs +++ b/frame/support/procedural/src/pallet/expand/documentation.rs @@ -1,6 +1,6 @@ // This file is part of Substrate. -// Copyright (C) 2023 Parity Technologies (UK) Ltd. +// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/frame/support/src/dispatch.rs b/frame/support/src/dispatch.rs index c26a3d8e71a36..2987b5846d701 100644 --- a/frame/support/src/dispatch.rs +++ b/frame/support/src/dispatch.rs @@ -2948,6 +2948,10 @@ macro_rules! decl_module { { $( $other_where_bounds )* } $( $error_type )* } + $crate::__impl_docs_metadata! { + $mod_type<$trait_instance: $trait_name $(, $instance: $instantiable)?> + { $( $other_where_bounds )* } + } $crate::__impl_module_constants_metadata ! { $mod_type<$trait_instance: $trait_name $(, $instance: $instantiable)?> { $( $other_where_bounds )* } @@ -3018,6 +3022,26 @@ macro_rules! __impl_error_metadata { }; } +/// Implement metadata for pallet documentation. +#[macro_export] +#[doc(hidden)] +macro_rules! __impl_docs_metadata { + ( + $mod_type:ident<$trait_instance:ident: $trait_name:ident$(, $instance:ident: $instantiable:path)?> + { $( $other_where_bounds:tt )* } + ) => { + impl<$trait_instance: $trait_name $(, $instance: $instantiable)?> $mod_type<$trait_instance $(, $instance)?> + where $( $other_where_bounds )* + { + #[doc(hidden)] + #[allow(dead_code)] + pub fn pallet_documentation_metadata() -> $crate::sp_std::vec::Vec<&'static str> { + $crate::sp_std::vec![] + } + } + }; +} + /// Implement metadata for module constants. #[macro_export] #[doc(hidden)] From 682adad711506f4fe917dac242b5ca387ec06f79 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 23 Feb 2023 18:57:34 +0200 Subject: [PATCH 4/9] frame/tests: Check exposed runtime metadata documentation Signed-off-by: Alexandru Vasile --- frame/support/test/tests/pallet.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frame/support/test/tests/pallet.rs b/frame/support/test/tests/pallet.rs index 7dd9aa0ddaf66..2dae721320571 100644 --- a/frame/support/test/tests/pallet.rs +++ b/frame/support/test/tests/pallet.rs @@ -98,6 +98,10 @@ impl SomeAssociation2 for u64 { } #[frame_support::pallet] +/// Pallet documentation +// Comments should not be included in the pallet documentation +#[pallet_doc("../../README.md")] +#[doc = include_str!("../../README.md")] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; @@ -1590,6 +1594,14 @@ fn metadata() { pretty_assertions::assert_eq!(actual_metadata.pallets, expected_metadata.pallets); } +#[test] +fn test_pallet_runtime_docs() { + let docs = crate::pallet::Pallet::::pallet_documentation_metadata(); + let readme = "Support code for the runtime.\n\nLicense: Apache-2.0"; + let expected = vec![" Pallet documentation", readme, readme]; + assert_eq!(docs, expected); +} + #[test] fn test_pallet_info_access() { assert_eq!(::name(), "System"); From 7e7661db5e7891b597f0db5af0ad6b4816f3b035 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 23 Feb 2023 19:02:12 +0200 Subject: [PATCH 5/9] frame/tests: Add UI tests for `pallet_doc` attribute Signed-off-by: Alexandru Vasile --- .../tests/pallet_ui/pallet_doc_arg_non_path.rs | 16 ++++++++++++++++ .../pallet_ui/pallet_doc_arg_non_path.stderr | 5 +++++ .../test/tests/pallet_ui/pallet_doc_empty.rs | 16 ++++++++++++++++ .../test/tests/pallet_ui/pallet_doc_empty.stderr | 5 +++++ .../tests/pallet_ui/pallet_doc_invalid_arg.rs | 16 ++++++++++++++++ .../pallet_ui/pallet_doc_invalid_arg.stderr | 5 +++++ .../tests/pallet_ui/pallet_doc_multiple_args.rs | 16 ++++++++++++++++ .../pallet_ui/pallet_doc_multiple_args.stderr | 5 +++++ 8 files changed, 84 insertions(+) create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.rs create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.stderr create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_empty.rs create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_empty.stderr create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.rs create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.stderr create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.rs create mode 100644 frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.stderr diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.rs b/frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.rs new file mode 100644 index 0000000000000..ef3097d23007d --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.rs @@ -0,0 +1,16 @@ +#[frame_support::pallet] +// Must receive a string literal pointing to a path +#[pallet_doc(X)] +mod pallet { + #[pallet::config] + pub trait Config: frame_system::Config + where + ::Index: From, + { + } + + #[pallet::pallet] + pub struct Pallet(core::marker::PhantomData); +} + +fn main() {} diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.stderr b/frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.stderr new file mode 100644 index 0000000000000..ba60479c07202 --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_arg_non_path.stderr @@ -0,0 +1,5 @@ +error: The `pallet_doc` received an unsupported argument. Supported format: `pallet_doc(PATH)` + --> tests/pallet_ui/pallet_doc_arg_non_path.rs:3:1 + | +3 | #[pallet_doc(X)] + | ^ diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_empty.rs b/frame/support/test/tests/pallet_ui/pallet_doc_empty.rs new file mode 100644 index 0000000000000..fe40806d2fa75 --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_empty.rs @@ -0,0 +1,16 @@ +#[frame_support::pallet] +// Expected one argument for the doc path. +#[pallet_doc] +mod pallet { + #[pallet::config] + pub trait Config: frame_system::Config + where + ::Index: From, + { + } + + #[pallet::pallet] + pub struct Pallet(core::marker::PhantomData); +} + +fn main() {} diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_empty.stderr b/frame/support/test/tests/pallet_ui/pallet_doc_empty.stderr new file mode 100644 index 0000000000000..d6a189d7918f5 --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_empty.stderr @@ -0,0 +1,5 @@ +error: The `pallet_doc` attribute must receive arguments as a list. Supported format: `pallet_doc(PATH)` + --> tests/pallet_ui/pallet_doc_empty.rs:3:1 + | +3 | #[pallet_doc] + | ^ diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.rs b/frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.rs new file mode 100644 index 0000000000000..8f0ccb3777a49 --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.rs @@ -0,0 +1,16 @@ +#[frame_support::pallet] +// Argument expected as list, not named value. +#[pallet_doc = "invalid"] +mod pallet { + #[pallet::config] + pub trait Config: frame_system::Config + where + ::Index: From, + { + } + + #[pallet::pallet] + pub struct Pallet(core::marker::PhantomData); +} + +fn main() {} diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.stderr b/frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.stderr new file mode 100644 index 0000000000000..9dd03978934a4 --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_invalid_arg.stderr @@ -0,0 +1,5 @@ +error: The `pallet_doc` attribute must receive arguments as a list. Supported format: `pallet_doc(PATH)` + --> tests/pallet_ui/pallet_doc_invalid_arg.rs:3:1 + | +3 | #[pallet_doc = "invalid"] + | ^ diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.rs b/frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.rs new file mode 100644 index 0000000000000..ffbed9d950799 --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.rs @@ -0,0 +1,16 @@ +#[frame_support::pallet] +// Supports only one argument. +#[pallet_doc("A", "B")] +mod pallet { + #[pallet::config] + pub trait Config: frame_system::Config + where + ::Index: From, + { + } + + #[pallet::pallet] + pub struct Pallet(core::marker::PhantomData); +} + +fn main() {} diff --git a/frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.stderr b/frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.stderr new file mode 100644 index 0000000000000..58ad75a0a2aec --- /dev/null +++ b/frame/support/test/tests/pallet_ui/pallet_doc_multiple_args.stderr @@ -0,0 +1,5 @@ +error: The `pallet_doc` attribute must receive only one argument. Supported format: `pallet_doc(PATH)` + --> tests/pallet_ui/pallet_doc_multiple_args.rs:3:1 + | +3 | #[pallet_doc("A", "B")] + | ^ From 6d38dbbb683d7065d2c7df0ebef0afc19161f83d Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 23 Feb 2023 19:03:06 +0200 Subject: [PATCH 6/9] frame/proc: Document pallet_doc attribute Signed-off-by: Alexandru Vasile --- frame/support/procedural/src/lib.rs | 43 +++++++++++++++++++ .../src/pallet/expand/documentation.rs | 13 +++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/frame/support/procedural/src/lib.rs b/frame/support/procedural/src/lib.rs index fec824107f123..c6bb250648adf 100644 --- a/frame/support/procedural/src/lib.rs +++ b/frame/support/procedural/src/lib.rs @@ -481,6 +481,49 @@ pub fn construct_runtime(input: TokenStream) -> TokenStream { /// /// /// See `frame_support::pallet` docs for more info. +/// +/// ## Runtime Metadata Documentation +/// +/// The documentation added to this pallet is included in the runtime metadata. +/// +/// The documentation can be defined in the following ways: +/// +/// ```ignore +/// #[pallet::pallet] +/// /// Documentation for pallet 1 +/// #[doc = "Documentation for pallet 2"] +/// #[doc = include_str!("../README.md")] +/// #[pallet_doc("../doc1.md")] +/// #[pallet_doc("../doc2.md")] +/// pub struct Pallet(_); +/// ``` +/// +/// The runtime metadata for this pallet contains the following +/// - " Documentation for pallet 1" (captured from `///`) +/// - "Documentation for pallet 2" (captured from `#[doc]`) +/// - content of ../README.md (captured from `#[doc]` with `include_str!`) +/// - content of "../doc1.md" (captured from `pallet_doc`) +/// - content of "../doc1.md" (captured from `pallet_doc`) +/// +/// ### `doc` attribute +/// +/// The value of the `doc` attribute is included in the runtime metadata, as well as +/// expanded on the pallet module. The previous example is expanded to: +/// +/// ```ignore +/// /// Documentation for pallet 1 +/// /// Documentation for pallet 2 +/// /// Content of README.md +/// pub struct Pallet(_); +/// ``` +/// +/// ### `pallet_doc` attribute +/// +/// The `pallet_doc` attribute can only be provided with one argument, +/// which is the file path that holds the documentation to be added to the metadata. +/// +/// Unlike the `doc` attribute, the documentation provided to the `proc_macro` attribute is +/// not inserted at the beginning of the module. #[proc_macro_attribute] pub fn pallet(attr: TokenStream, item: TokenStream) -> TokenStream { pallet::pallet(attr, item) diff --git a/frame/support/procedural/src/pallet/expand/documentation.rs b/frame/support/procedural/src/pallet/expand/documentation.rs index 131ef0042ba32..f9e67a12809e4 100644 --- a/frame/support/procedural/src/pallet/expand/documentation.rs +++ b/frame/support/procedural/src/pallet/expand/documentation.rs @@ -143,20 +143,17 @@ impl ToTokens for DocMetaValue { /// #[doc = "Documentation for pallet"] /// #[doc = include_str!("../README.md")] /// #[pallet_doc("../documentation1.md")] -/// #[pallet_doc("../documentation1.md")] +/// #[pallet_doc("../documentation2.md")] /// pub mod pallet {} /// ``` /// /// # pallet_doc /// -/// The `pallet_doc` attribute accepts only one argument: the path to a file -/// containing the documentation that should be included in the metadata. -/// -/// Argument is similar to `include_str`. +/// The `pallet_doc` attribute can only be provided with one argument, +/// which is the file path that holds the documentation to be added to the metadata. /// -/// The documentation parsed by this attribute is not expanded on the pallet -/// definition. In contract, #[doc = include_str!("../README.md")] is expanded -/// on the pallet definition as well as included in the metadata. +/// Unlike the `doc` attribute, the documentation provided to the `proc_macro` attribute is +/// not inserted at the beginning of the module. pub fn expand_documentation(def: &mut Def) -> proc_macro2::TokenStream { let frame_support = &def.frame_support; let type_impl_gen = &def.type_impl_generics(proc_macro2::Span::call_site()); From c5c894f932e29e624f9f23d0a61628b2f72289a5 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 9 Mar 2023 17:57:10 +0200 Subject: [PATCH 7/9] frame/support: Use `derive_syn_parse` Signed-off-by: Alexandru Vasile --- .../src/pallet/expand/documentation.rs | 98 ++++++++++--------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/frame/support/procedural/src/pallet/expand/documentation.rs b/frame/support/procedural/src/pallet/expand/documentation.rs index f9e67a12809e4..e158448a89711 100644 --- a/frame/support/procedural/src/pallet/expand/documentation.rs +++ b/frame/support/procedural/src/pallet/expand/documentation.rs @@ -16,19 +16,22 @@ // limitations under the License. use crate::pallet::Def; -use proc_macro2::{Ident, TokenStream}; +use derive_syn_parse::Parse; +use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ - parenthesized, parse::{self, Parse, ParseStream}, spanned::Spanned, - Attribute, Lit, Token, + Attribute, Lit, }; const DOC: &'static str = "doc"; -const INCLUDE_STR: &'static str = "include_str"; const PALLET_DOC: &'static str = "pallet_doc"; +mod keywords { + syn::custom_keyword!(include_str); +} + /// Get the documentation file path from the `pallet_doc` attribute. /// /// Supported format: @@ -38,23 +41,22 @@ fn parse_pallet_doc_value(attr: &Attribute) -> syn::Result { let meta = attr.parse_meta()?; let syn::Meta::List(metalist) = meta else { - let msg = - "The `pallet_doc` attribute must receive arguments as a list. Supported format: `pallet_doc(PATH)`"; - return Err(syn::Error::new(span, msg)) - }; + let msg = "The `pallet_doc` attribute must receive arguments as a list. Supported format: `pallet_doc(PATH)`"; + return Err(syn::Error::new(span, msg)) + }; let paths: Vec<_> = metalist - .nested - .into_iter() - .map(|nested| { - let syn::NestedMeta::Lit(lit) = nested else { - let msg = "The `pallet_doc` received an unsupported argument. Supported format: `pallet_doc(PATH)`"; - return Err(syn::Error::new(span, msg)) - }; - - Ok(lit) - }) - .collect::>()?; + .nested + .into_iter() + .map(|nested| { + let syn::NestedMeta::Lit(lit) = nested else { + let msg = "The `pallet_doc` received an unsupported argument. Supported format: `pallet_doc(PATH)`"; + return Err(syn::Error::new(span, msg)) + }; + + Ok(lit) + }) + .collect::>()?; if paths.len() != 1 { let msg = "The `pallet_doc` attribute must receive only one argument. Supported format: `pallet_doc(PATH)`"; @@ -71,14 +73,43 @@ fn parse_pallet_doc_value(attr: &Attribute) -> syn::Result { /// - `#[doc = include_str!(PATH)]`: Documentation obtained from a path fn parse_doc_value(attr: &Attribute) -> Option { let Some(ident) = attr.path.get_ident() else { - return None - }; + return None + }; if ident != DOC { return None } - let parser = |input: ParseStream| DocMetaValue::parse(input); - parse::Parser::parse2(parser, attr.tokens.clone()).ok() + let parser = |input: ParseStream| DocParser::parse(input); + let result = parse::Parser::parse2(parser, attr.tokens.clone()).ok()?; + + if let Some(lit) = result.lit { + Some(DocMetaValue::Lit(lit)) + } else if let Some(include_doc) = result.include_doc { + Some(DocMetaValue::Path(include_doc.lit)) + } else { + None + } +} + +/// Parse the include_str attribute. +#[derive(Debug, Parse)] +struct IncludeDocParser { + _include_str: keywords::include_str, + _eq_token: syn::token::Bang, + #[paren] + _paren: syn::token::Paren, + #[inside(_paren)] + lit: Lit, +} + +/// Parse the doc literal. +#[derive(Debug, Parse)] +struct DocParser { + _eq_token: syn::token::Eq, + #[peek(Lit)] + lit: Option, + #[parse_if(lit.is_none())] + include_doc: Option, } /// Supported documentation tokens. @@ -96,27 +127,6 @@ enum DocMetaValue { Path(Lit), } -impl Parse for DocMetaValue { - fn parse(input: ParseStream) -> syn::Result { - let _token: Token![=] = input.parse()?; - - if input.peek(Lit) { - return input.parse().map(DocMetaValue::Lit) - } - - let ident: Ident = input.parse()?; - if ident != INCLUDE_STR { - return Err(input.error("expected include_str ident")) - } - let _token: Token![!] = input.parse()?; - - // We must have a path literal inside `(...)` - let content; - parenthesized!(content in input); - content.parse().map(DocMetaValue::Path) - } -} - impl ToTokens for DocMetaValue { fn to_tokens(&self, tokens: &mut TokenStream) { match self { From bb8cd03a4ae5c97f6f28c01b1de771ab4e866b23 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Mon, 13 Mar 2023 14:20:56 +0200 Subject: [PATCH 8/9] Update frame/support/procedural/src/lib.rs Co-authored-by: Niklas Adolfsson --- frame/support/procedural/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/support/procedural/src/lib.rs b/frame/support/procedural/src/lib.rs index c6bb250648adf..9f8c2f94f0755 100644 --- a/frame/support/procedural/src/lib.rs +++ b/frame/support/procedural/src/lib.rs @@ -503,7 +503,7 @@ pub fn construct_runtime(input: TokenStream) -> TokenStream { /// - "Documentation for pallet 2" (captured from `#[doc]`) /// - content of ../README.md (captured from `#[doc]` with `include_str!`) /// - content of "../doc1.md" (captured from `pallet_doc`) -/// - content of "../doc1.md" (captured from `pallet_doc`) +/// - content of "../doc2.md" (captured from `pallet_doc`) /// /// ### `doc` attribute /// From bf9400b97623505f1f77958aadd328f7fe9fc1a3 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 13 Mar 2023 14:32:29 +0200 Subject: [PATCH 9/9] frame/support: Improve documentation Signed-off-by: Alexandru Vasile --- frame/support/procedural/src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frame/support/procedural/src/lib.rs b/frame/support/procedural/src/lib.rs index 9f8c2f94f0755..1d5dde20e2034 100644 --- a/frame/support/procedural/src/lib.rs +++ b/frame/support/procedural/src/lib.rs @@ -517,13 +517,21 @@ pub fn construct_runtime(input: TokenStream) -> TokenStream { /// pub struct Pallet(_); /// ``` /// +/// If you want to specify the file from which the documentation is loaded, you can use the +/// `include_str` macro. However, if you only want the documentation to be included in the +/// runtime metadata, use the `pallet_doc` attribute. +/// /// ### `pallet_doc` attribute /// +/// Unlike the `doc` attribute, the documentation provided to the `pallet_doc` attribute is +/// not inserted on the module. +/// /// The `pallet_doc` attribute can only be provided with one argument, /// which is the file path that holds the documentation to be added to the metadata. /// -/// Unlike the `doc` attribute, the documentation provided to the `proc_macro` attribute is -/// not inserted at the beginning of the module. +/// This approach is beneficial when you use the `include_str` macro at the beginning of the file +/// and want that documentation to extend to the runtime metadata, without reiterating the +/// documentation on the module itself. #[proc_macro_attribute] pub fn pallet(attr: TokenStream, item: TokenStream) -> TokenStream { pallet::pallet(attr, item)