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;