docpos/
lib.rs

1#![cfg_attr(not(debug_assertions),allow(non_snake_case,non_upper_case_globals,non_camel_case_types))]
2#![cfg_attr(    debug_assertions ,allow(non_snake_case,non_upper_case_globals,non_camel_case_types,unused_imports,unused_mut,unused_variables,dead_code,unused_assignments,unused_macros))]
3#![doc= include_str!("../Readme.md")]
4//! ## Documenting Generics
5//! Generic parameters can be documented with doc comments just as the arguments
6//! can be:
7//! ```rust
8//! use docpos::docpos;
9//!
10//! #[docpos]
11//! fn frobnicate<
12//! S, /// some comment goes here
13//! T> (
14//! frobnicator: T,/// the value being frobnicated
15//! frobnicant : S,/// the frobnicant
16//! ) -> T {
17//!    todo!()
18//! }
19//! ```
20//!
21//! This generates an additional section for the generic parameters right
22//! after the arguments section (if it exists).
23//! All types of generic arguments, including lifetimes and const-generics
24//! can be documented like this.
25use quote::{quote, ToTokens};
26use syn::{parse_macro_input, Attribute, ItemFn, ItemStruct, ItemEnum, Ident};
27use util::{
28    extract_documented_generics, extract_documented_parameters, extract_fn_doc_attrs, make_doc_block,
29    extract_struct_doc_attrs
30};
31use docpos_fn::docpos_fn;
32use docpos_struct::docpos_struct;
33use docpos_enum::docpos_enum;
34use helper::*;
35mod util;
36mod helper;
37mod util_struct;
38mod util_fn;
39mod util_enum;
40mod docpos_struct;
41mod docpos_enum;
42mod docpos_fn;
43
44use indoc::formatdoc;
45
46/// parameter section macro name
47const PARAM_SECTION: &str = "parameters_section";
48/// the name of the main macro in this crate
49const ROXYGEN_MACRO: &str = "roxygen";
50
51mod mhelp {
52  // helper macro "try" on a syn::Error, so that we can return it as a token stream
53    macro_rules! try2 {
54        ($ex:expr) => {
55            match $ex {
56                Ok(val) => val,
57                Err(err) => return err.into_compile_error().into(),
58            }
59        };
60    }
61    pub(crate) use try2;
62}
63use mhelp::try2 as try2;
64
65#[proc_macro_attribute]
66/// the principal attribute inside this crate that lets us document function arguments
67pub fn roxygen(
68    _attr: proc_macro::TokenStream,
69    item: proc_macro::TokenStream,
70) -> proc_macro::TokenStream {
71    let mut function: ItemFn = parse_macro_input!(item as ItemFn);
72
73    try2!(function.attrs.iter_mut().try_for_each(|attr| {
74        if is_roxygen_main(attr) {
75            Err(syn::Error::new_spanned(
76                attr,
77                "Duplicate attribute. This attribute must only appear once.",
78            ))
79        } else {
80            Ok(())
81        }
82    }));
83
84    // extrac the doc attributes on the function itself
85    let function_docs = try2!(extract_fn_doc_attrs(&mut function.attrs));
86
87    let documented_params = try2!(extract_documented_parameters(
88        function.sig.inputs.iter_mut()
89    ));
90
91    let documented_generics = try2!(extract_documented_generics(&mut function.sig.generics));
92
93    let has_documented_params = !documented_params.is_empty();
94    let has_documented_generics = !documented_generics.is_empty();
95
96    if !has_documented_params && !has_documented_generics {
97        return syn::Error::new_spanned(
98            function.sig.ident,
99            "Function has no documented parameters or generics.\nDocument at least one function parameter or generic.",
100        )
101        .into_compile_error()
102        .into();
103    }
104
105    let parameter_doc_block = make_doc_block("Parameters", documented_params);
106    let generics_doc_block = make_doc_block("Generics", documented_generics);
107
108    let docs_before = function_docs.before_args_section;
109    let docs_after = function_docs.after_args_section;
110    let maybe_empty_doc_line = if !docs_after.is_empty() {
111        Some(quote! {#[doc=""]})
112    } else {
113        None
114    };
115
116    quote! {
117        #(#docs_before)*
118        #parameter_doc_block
119        #generics_doc_block
120        #maybe_empty_doc_line
121        #(#docs_after)*
122        #function
123    }
124    .into()
125}
126
127use syn::Result;
128use syn::ext::IdentExt;
129use syn::parse::{Parse,ParseStream};
130use core::fmt;
131#[derive(Debug)]
132struct IdentAny {pub ident:String}
133impl fmt::Display for IdentAny {fn fmt   (&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "{}", self.ident)}}
134impl AsRef<str>   for IdentAny {fn as_ref(&self                        ) -> &str        {               &self.ident}}
135impl Parse        for IdentAny {fn parse(input:ParseStream) -> Result<IdentAny> {
136    let lookahead = input.lookahead1();
137    if  lookahead.peek(Ident::peek_any) {
138        let name = input.call(Ident::parse_any)?;
139        Ok(Self {ident:name.to_string()})
140    } else {Err(lookahead.error())}  }
141}
142
143#[proc_macro_attribute]
144/// the principal attribute inside this crate that lets us document function or struct arguments, but after them, not before
145/// Allows either `#[docpos(struct)]` (or 'fn', 'enum') literal syntax or just `#[docpos]` for autodetection
146pub fn docpos(attr: proc_macro::TokenStream // attributes of macro args: docpos(arg) would be Ident (though keywords can't be parsed by def)
147    ,         item: proc_macro::TokenStream,
148    )            -> proc_macro::TokenStream {
149    match syn::parse::<IdentAny>(attr) {
150        Ok (id)          => {match id.to_string().as_ref() {// 1 Parse 'ident' arguments first
151            "struct"     => {return docpos_struct(parse_macro_input!(item as ItemStruct),false)},
152            "struct_sect"=> {return docpos_struct(parse_macro_input!(item as ItemStruct),true )},
153            "enum"       => {return docpos_enum  (parse_macro_input!(item as ItemEnum  ),false)},
154            "enum_sect"  => {return docpos_enum  (parse_macro_input!(item as ItemEnum  ),true )},
155            "fn"         => {return docpos_fn    (parse_macro_input!(item as ItemFn    ))},
156            _            => {let errmsg=format!("Expected either 'struct','fn','enum', got '{}'\n(or use '#[docpos]' without an argument for auto-detection)",id);
157                return  quote! {compile_error!(#errmsg)}.into();}
158        }},
159        Err(_err   ) => {let (e_struct, e_enum, e_fn);            // 2 Detect via parsing the item
160            match syn::parse::<ItemStruct>(item.clone()) {Ok(item)=>{return docpos_struct(item,false)}, Err(err)=>{e_struct=err},};
161            match syn::parse::<ItemEnum  >(item.clone()) {Ok(item)=>{return docpos_enum  (item,false)}, Err(err)=>{e_enum  =err},};
162            match syn::parse::<ItemFn    >(item        ) {Ok(item)=>{return docpos_fn    (item      )}, Err(err)=>{e_fn    =err},};
163            let errmsg = formatdoc!(r#"Parsing ℯ as
164                Struct: {e_struct},
165                Enum  : {e_enum},
166                Fn    : {e_fn}"#);                                   return quote!{compile_error!(#errmsg)}.into();
167        }
168    }
169}
170
171
172// this is to expose the helper attribute #[arguments_section].
173// The only logic about this attribute that this here function includes is
174// to make sure that this attribute is not placed before the #[roxygen]
175// attribute. All other logic is handled in the roxygen macro itself.
176/// a helper attribute that dictates the placement of the section documenting
177/// the function arguments
178#[proc_macro_attribute]
179pub fn parameters_section(
180    _attr: proc_macro::TokenStream,
181    item: proc_macro::TokenStream,
182) -> proc_macro::TokenStream {
183    let function: ItemFn = parse_macro_input!(item as ItemFn);
184
185    // enforce that this macro comes after roxygen, which means it
186    // cannot see the roxygen attribute
187    let maybe_roxygen = function.attrs.iter().find(|attr| is_roxygen_main(attr));
188    if let Some(attr) = maybe_roxygen {
189        syn::Error::new_spanned(attr,"The #[roxygen] attribute must come before the parameters_section attribute.\nPlace it before any of the doc comments for the function.").into_compile_error().into()
190    } else {
191        function.to_token_stream().into()
192    }
193}
194
195/// check whether an attribute is the arguments section attribute.
196/// Stick this into it's own function so I can change the logic
197//@note(geo) this logic won't work if the crate is renamed
198#[inline(always)]
199fn is_parameters_section(attr: &Attribute) -> bool {
200    let path = attr.path();
201
202    if path.is_ident(PARAM_SECTION) {
203        true
204    } else {
205        // checks for (::)roxygen::param_section
206        path.segments.len() == 2
207            && path.segments[0].ident == DOCPOS_CRATE
208            && path.segments[1].ident == PARAM_SECTION
209    }
210}
211
212/// check whether an attribute is the raw #[roxygen] main attribute.
213/// Stuck into this function, so I can refactor this logic
214//@note(geo) this logic won't work if the crate is renamed
215#[inline(always)]
216fn is_roxygen_main(attr: &Attribute) -> bool {
217    let path = attr.path();
218
219    if path.is_ident(ROXYGEN_MACRO) {
220        true
221    } else {
222        // checks for (::)roxygen::roxygen
223        path.segments.len() == 2
224            && path.segments[0].ident == DOCPOS_CRATE
225            && path.segments[1].ident == ROXYGEN_MACRO
226    }
227}