clean_macro_docs/
lib.rs

1//! Hide internal rules when documenting `macro_rules!` macros.
2//!
3//! When generating docs for `macro_rules!` macros, `rustdoc` will include every
4//! rule, including internal rules that are only supposed to be called from within
5//! your macro. The `clean_docs` attribute will hide your internal rules from
6//! `rustdoc`.
7//!
8//! # Example:
9//! ```
10//! # use clean_macro_docs::clean_docs;
11//! #[macro_export]
12//! macro_rules! messy {
13//!     (@impl $e:expr) => {
14//!         format!("{}", $e)
15//!     };
16//!     ($e:expr) => {
17//!         messy!(@impl $e)
18//!     };
19//! }
20//!
21//! #[clean_docs]
22//! #[macro_export]
23//! macro_rules! clean {
24//!     (@impl $e:expr) => {
25//!         format!("{}", $e)
26//!     };
27//!     ($e:expr) => {
28//!         clean!(@impl $e)
29//!     };
30//! }
31//! ```
32//!
33//! would be documented as
34//! ```
35//! macro_rules! mac {
36//!     ($e:expr) => { ... };
37//! }
38//! ```
39//! # How does it work?
40//! The `clean!` macro above is transformed into
41//! ```
42//! #[macro_export]
43//! macro_rules! clean {
44//!     ($e:expr) => {
45//!         $crate::__clean!(@impl $e)
46//!     };
47//! }
48//!
49//! #[macro_export]
50//! macro_rules! __clean {
51//!     (@impl $e:expr) => {
52//!         format!("{}", $e)
53//!     };
54//! }
55//!
56//! macro_rules! clean {
57//!     (@impl $e:expr) => {
58//!         format!("{}", $e)
59//!     };
60//!     ($e:expr) => {
61//!         clean!(@impl $e)
62//!     };
63//! }
64//! ```
65//!
66//! The last, non-`macro_export`ed macro is there becuase Rust doesn't allow
67//! macro-expanded macros to be invoked by absolute path (i.e. `$crate::__clean`).
68//!
69//! The solution is to shadow the `macro_export`ed macro with a local version
70//! that doesn't use absolute paths.
71//!
72//! # Arguments
73//! You can use these optional arguments to configure `clean_macro`.
74//!
75//! ```
76//! # use clean_macro_docs::clean_docs;
77//! #[clean_docs(impl = "#internal", internal = "__internal_mac")]
78//! # macro_rules! mac { () => {} }
79//! ```
80//!
81//! ## `impl`
82//! A string representing the "flag" at the begining of an internal rule. Defaults to `"@"`.
83//!
84//! ```
85//! # use clean_macro_docs::clean_docs;
86//! #[clean_docs(impl = "#internal")]
87//! #[macro_export]
88//! macro_rules! mac {
89//!     (#internal $e:expr) => {
90//!         format!("{}", $e)
91//!     };
92//!     ($e:expr) => {
93//!         mac!(#internal $e)
94//!     };
95//! }
96//! ```
97//!
98//! ## `internal`
99//! A string representing the identifier to use for the internal version of your macro.
100//! By default `clean_docs` prepends `__` (two underscores) to the main macro's identifier.
101//!
102//! ```
103//! # use clean_macro_docs::clean_docs;
104//! #[clean_docs(internal = "__internal_mac")]
105//! #[macro_export]
106//! macro_rules! mac {
107//!     (@impl $e:expr) => {
108//!         format!("{}", $e)
109//!     };
110//!     ($e:expr) => {
111//!         mac!(@impl $e)
112//!     };
113//! }
114//! ```
115
116extern crate proc_macro;
117extern crate proc_macro2;
118
119use proc_macro2::{Punct, Spacing, TokenStream, TokenTree};
120use quote::{format_ident, quote, quote_spanned};
121use std::str::FromStr;
122use syn::punctuated::Punctuated;
123use syn::spanned::Spanned;
124use syn::{parse_macro_input, AttributeArgs, Ident, Lit, Meta, NestedMeta, Token};
125
126mod macro_rules;
127mod replace_macro_invocs;
128
129use macro_rules::*;
130use replace_macro_invocs::replace_macro_invocs;
131
132#[proc_macro_attribute]
133pub fn clean_docs(
134    args: proc_macro::TokenStream,
135    item: proc_macro::TokenStream,
136) -> proc_macro::TokenStream {
137    let args = parse_macro_input!(args as AttributeArgs);
138    let mac_rules = parse_macro_input!(item as MacroRules);
139    clean_docs_impl(args, mac_rules).into()
140}
141
142fn clean_docs_impl(args: AttributeArgs, mut mac_rules: MacroRules) -> TokenStream {
143    let mut priv_marker: Option<TokenStream> = None;
144    let mut priv_ident: Option<Ident> = None;
145
146    for arg in args {
147        if let NestedMeta::Meta(Meta::NameValue(arg)) = arg {
148            match arg
149                .path
150                .get_ident()
151                .map(Ident::to_string)
152                .as_ref()
153                .map(String::as_str)
154            {
155                Some("impl") => {
156                    if let Lit::Str(val) = &arg.lit {
157                        priv_marker = Some({
158                            if let Ok(priv_marker) = TokenStream::from_str(&val.value()) {
159                                priv_marker
160                            } else {
161                                return quote_spanned! {
162                                    arg.lit.span()=> compile_error!("invalid tokens");
163                                };
164                            }
165                        })
166                    } else {
167                        return quote_spanned! {
168                            arg.lit.span()=> compile_error!("expected string");
169                        };
170                    }
171                }
172                Some("internal") => {
173                    if let Lit::Str(val) = &arg.lit {
174                        priv_ident = Some({
175                            if let Ok(priv_ident) = val.parse() {
176                                priv_ident
177                            } else {
178                                return quote_spanned! {
179                                    arg.lit.span()=> compile_error!("expected identifier");
180                                };
181                            }
182                        })
183                    } else {
184                        return quote_spanned! {
185                            arg.lit.span()=> compile_error!("expected string");
186                        };
187                    }
188                }
189                _ => {
190                    let arg_path = &arg.path;
191                    let arg_str = quote!(#arg_path).to_string();
192                    return quote_spanned! {
193                        arg.span()=> compile_error!(concat!("invalid argument: ", #arg_str));
194                    };
195                }
196            };
197        } else {
198            let arg_str = quote!(#arg).to_string();
199            return quote_spanned! {
200                arg.span()=> compile_error!(concat!("invalid argument: ", #arg_str));
201            };
202        }
203    }
204
205    // Clone item, to be reimitted unmodified without #[macro_export]
206    let mut original = mac_rules.clone();
207
208    let pub_ident = &mac_rules.ident;
209
210    // Default values
211    let priv_marker = priv_marker
212        .unwrap_or_else(|| TokenStream::from(TokenTree::Punct(Punct::new('@', Spacing::Joint))));
213    let priv_ident = priv_ident.unwrap_or_else(|| format_ident!("__{}", pub_ident));
214
215    let mut pub_rules = Punctuated::<MacroRulesRule, Token![;]>::new();
216    let mut priv_rules = Punctuated::<MacroRulesRule, Token![;]>::new();
217
218    for mut rule in mac_rules.rules {
219        rule.body = replace_macro_invocs(rule.body, pub_ident, &priv_ident, &priv_marker);
220        if rule.rule.to_string().starts_with(&priv_marker.to_string()) {
221            priv_rules.push(rule);
222        } else {
223            pub_rules.push(rule);
224        }
225    }
226
227    if pub_rules.is_empty() {
228        return quote! {
229            compile_error!("no public rules");
230        };
231    }
232
233    if priv_rules.is_empty() {
234        return quote! {
235            #original
236        };
237    }
238
239    if original.rules.trailing_punct() {
240        priv_rules.push_punct(<Token![;]>::default());
241        pub_rules.push_punct(<Token![;]>::default());
242    }
243
244    mac_rules.rules = pub_rules;
245
246    let mut priv_mac_rules = MacroRules {
247        ident: priv_ident,
248        rules: priv_rules,
249        ..mac_rules.clone()
250    };
251
252    // Remove doc comments (and other doc attrs) from private version
253    priv_mac_rules.attrs.retain(|attr| {
254        if let Some(ident) = attr.path.get_ident() {
255            ident.to_string() != "doc"
256        } else {
257            true
258        }
259    });
260
261    // Remove #[macro_export] and doc comments (and other doc attrs) from crate-internal version
262    original.attrs.retain(|attr| {
263        if let Some(ident) = attr.path.get_ident() {
264            ident.to_string() != "macro_export" && ident.to_string() != "doc"
265        } else {
266            true
267        }
268    });
269
270    let gen = quote! {
271        #mac_rules
272        #[doc(hidden)]
273        #priv_mac_rules
274
275        #[allow(unused_macros)]
276        #original
277    };
278    gen.into()
279}
280
281#[cfg(test)]
282mod tests;