|
| 1 | +// This file is part of Substrate. |
| 2 | + |
| 3 | +// Copyright (C) Parity Technologies (UK) Ltd. |
| 4 | +// SPDX-License-Identifier: Apache-2.0 |
| 5 | + |
| 6 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +// you may not use this file except in compliance with the License. |
| 8 | +// You may obtain a copy of the License at |
| 9 | +// |
| 10 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +// |
| 12 | +// Unless required by applicable law or agreed to in writing, software |
| 13 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +// See the License for the specific language governing permissions and |
| 16 | +// limitations under the License. |
| 17 | + |
| 18 | +use crate::pallet::Def; |
| 19 | +use derive_syn_parse::Parse; |
| 20 | +use proc_macro2::TokenStream; |
| 21 | +use quote::ToTokens; |
| 22 | +use syn::{ |
| 23 | + parse::{self, Parse, ParseStream}, |
| 24 | + spanned::Spanned, |
| 25 | + Attribute, Lit, |
| 26 | +}; |
| 27 | + |
| 28 | +const DOC: &'static str = "doc"; |
| 29 | +const PALLET_DOC: &'static str = "pallet_doc"; |
| 30 | + |
| 31 | +mod keywords { |
| 32 | + syn::custom_keyword!(include_str); |
| 33 | +} |
| 34 | + |
| 35 | +/// Get the documentation file path from the `pallet_doc` attribute. |
| 36 | +/// |
| 37 | +/// Supported format: |
| 38 | +/// `#[pallet_doc(PATH)]`: The path of the file from which the documentation is loaded |
| 39 | +fn parse_pallet_doc_value(attr: &Attribute) -> syn::Result<DocMetaValue> { |
| 40 | + let span = attr.span(); |
| 41 | + |
| 42 | + let meta = attr.parse_meta()?; |
| 43 | + let syn::Meta::List(metalist) = meta else { |
| 44 | + let msg = "The `pallet_doc` attribute must receive arguments as a list. Supported format: `pallet_doc(PATH)`"; |
| 45 | + return Err(syn::Error::new(span, msg)) |
| 46 | + }; |
| 47 | + |
| 48 | + let paths: Vec<_> = metalist |
| 49 | + .nested |
| 50 | + .into_iter() |
| 51 | + .map(|nested| { |
| 52 | + let syn::NestedMeta::Lit(lit) = nested else { |
| 53 | + let msg = "The `pallet_doc` received an unsupported argument. Supported format: `pallet_doc(PATH)`"; |
| 54 | + return Err(syn::Error::new(span, msg)) |
| 55 | + }; |
| 56 | + |
| 57 | + Ok(lit) |
| 58 | + }) |
| 59 | + .collect::<syn::Result<_>>()?; |
| 60 | + |
| 61 | + if paths.len() != 1 { |
| 62 | + let msg = "The `pallet_doc` attribute must receive only one argument. Supported format: `pallet_doc(PATH)`"; |
| 63 | + return Err(syn::Error::new(span, msg)) |
| 64 | + } |
| 65 | + |
| 66 | + Ok(DocMetaValue::Path(paths[0].clone())) |
| 67 | +} |
| 68 | + |
| 69 | +/// Get the value from the `doc` comment attribute: |
| 70 | +/// |
| 71 | +/// Supported formats: |
| 72 | +/// - `#[doc = "A doc string"]`: Documentation as a string literal |
| 73 | +/// - `#[doc = include_str!(PATH)]`: Documentation obtained from a path |
| 74 | +fn parse_doc_value(attr: &Attribute) -> Option<DocMetaValue> { |
| 75 | + let Some(ident) = attr.path.get_ident() else { |
| 76 | + return None |
| 77 | + }; |
| 78 | + if ident != DOC { |
| 79 | + return None |
| 80 | + } |
| 81 | + |
| 82 | + let parser = |input: ParseStream| DocParser::parse(input); |
| 83 | + let result = parse::Parser::parse2(parser, attr.tokens.clone()).ok()?; |
| 84 | + |
| 85 | + if let Some(lit) = result.lit { |
| 86 | + Some(DocMetaValue::Lit(lit)) |
| 87 | + } else if let Some(include_doc) = result.include_doc { |
| 88 | + Some(DocMetaValue::Path(include_doc.lit)) |
| 89 | + } else { |
| 90 | + None |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +/// Parse the include_str attribute. |
| 95 | +#[derive(Debug, Parse)] |
| 96 | +struct IncludeDocParser { |
| 97 | + _include_str: keywords::include_str, |
| 98 | + _eq_token: syn::token::Bang, |
| 99 | + #[paren] |
| 100 | + _paren: syn::token::Paren, |
| 101 | + #[inside(_paren)] |
| 102 | + lit: Lit, |
| 103 | +} |
| 104 | + |
| 105 | +/// Parse the doc literal. |
| 106 | +#[derive(Debug, Parse)] |
| 107 | +struct DocParser { |
| 108 | + _eq_token: syn::token::Eq, |
| 109 | + #[peek(Lit)] |
| 110 | + lit: Option<Lit>, |
| 111 | + #[parse_if(lit.is_none())] |
| 112 | + include_doc: Option<IncludeDocParser>, |
| 113 | +} |
| 114 | + |
| 115 | +/// Supported documentation tokens. |
| 116 | +#[derive(Debug)] |
| 117 | +enum DocMetaValue { |
| 118 | + /// Documentation with string literals. |
| 119 | + /// |
| 120 | + /// `#[doc = "Lit"]` |
| 121 | + Lit(Lit), |
| 122 | + /// Documentation with `include_str!` macro. |
| 123 | + /// |
| 124 | + /// The string literal represents the file `PATH`. |
| 125 | + /// |
| 126 | + /// `#[doc = include_str!(PATH)]` |
| 127 | + Path(Lit), |
| 128 | +} |
| 129 | + |
| 130 | +impl ToTokens for DocMetaValue { |
| 131 | + fn to_tokens(&self, tokens: &mut TokenStream) { |
| 132 | + match self { |
| 133 | + DocMetaValue::Lit(lit) => lit.to_tokens(tokens), |
| 134 | + DocMetaValue::Path(path_lit) => { |
| 135 | + let decl = quote::quote!(include_str!(#path_lit)); |
| 136 | + tokens.extend(decl) |
| 137 | + }, |
| 138 | + } |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +/// Extract the documentation from the given pallet definition |
| 143 | +/// to include in the runtime metadata. |
| 144 | +/// |
| 145 | +/// Implement a `pallet_documentation_metadata` function to fetch the |
| 146 | +/// documentation that is included in the metadata. |
| 147 | +/// |
| 148 | +/// The documentation is placed at the top of the module similar to: |
| 149 | +/// |
| 150 | +/// ```ignore |
| 151 | +/// #[pallet] |
| 152 | +/// /// Documentation for pallet |
| 153 | +/// #[doc = "Documentation for pallet"] |
| 154 | +/// #[doc = include_str!("../README.md")] |
| 155 | +/// #[pallet_doc("../documentation1.md")] |
| 156 | +/// #[pallet_doc("../documentation2.md")] |
| 157 | +/// pub mod pallet {} |
| 158 | +/// ``` |
| 159 | +/// |
| 160 | +/// # pallet_doc |
| 161 | +/// |
| 162 | +/// The `pallet_doc` attribute can only be provided with one argument, |
| 163 | +/// which is the file path that holds the documentation to be added to the metadata. |
| 164 | +/// |
| 165 | +/// Unlike the `doc` attribute, the documentation provided to the `proc_macro` attribute is |
| 166 | +/// not inserted at the beginning of the module. |
| 167 | +pub fn expand_documentation(def: &mut Def) -> proc_macro2::TokenStream { |
| 168 | + let frame_support = &def.frame_support; |
| 169 | + let type_impl_gen = &def.type_impl_generics(proc_macro2::Span::call_site()); |
| 170 | + let type_use_gen = &def.type_use_generics(proc_macro2::Span::call_site()); |
| 171 | + let pallet_ident = &def.pallet_struct.pallet; |
| 172 | + let where_clauses = &def.config.where_clause; |
| 173 | + |
| 174 | + // TODO: Use [drain_filter](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter) when it is stable. |
| 175 | + |
| 176 | + // The `pallet_doc` attributes are excluded from the generation of the pallet, |
| 177 | + // but they are included in the runtime metadata. |
| 178 | + let mut pallet_docs = Vec::with_capacity(def.item.attrs.len()); |
| 179 | + let mut index = 0; |
| 180 | + while index < def.item.attrs.len() { |
| 181 | + let attr = &def.item.attrs[index]; |
| 182 | + if let Some(ident) = attr.path.get_ident() { |
| 183 | + if ident == PALLET_DOC { |
| 184 | + let elem = def.item.attrs.remove(index); |
| 185 | + pallet_docs.push(elem); |
| 186 | + // Do not increment the index, we have just removed the |
| 187 | + // element from the attributes. |
| 188 | + continue |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + index += 1; |
| 193 | + } |
| 194 | + |
| 195 | + // Capture the `#[doc = include_str!("../README.md")]` and `#[doc = "Documentation"]`. |
| 196 | + let mut docs: Vec<_> = def.item.attrs.iter().filter_map(parse_doc_value).collect(); |
| 197 | + |
| 198 | + // Capture the `#[pallet_doc("../README.md")]`. |
| 199 | + let pallet_docs: Vec<_> = match pallet_docs |
| 200 | + .into_iter() |
| 201 | + .map(|attr| parse_pallet_doc_value(&attr)) |
| 202 | + .collect::<syn::Result<_>>() |
| 203 | + { |
| 204 | + Ok(docs) => docs, |
| 205 | + Err(err) => return err.into_compile_error(), |
| 206 | + }; |
| 207 | + |
| 208 | + docs.extend(pallet_docs); |
| 209 | + |
| 210 | + quote::quote!( |
| 211 | + impl<#type_impl_gen> #pallet_ident<#type_use_gen> #where_clauses{ |
| 212 | + |
| 213 | + #[doc(hidden)] |
| 214 | + pub fn pallet_documentation_metadata() |
| 215 | + -> #frame_support::sp_std::vec::Vec<&'static str> |
| 216 | + { |
| 217 | + #frame_support::sp_std::vec![ #( #docs ),* ] |
| 218 | + } |
| 219 | + } |
| 220 | + ) |
| 221 | +} |
0 commit comments