macro_vis/
lib.rs

1//! This crate provides an attribute for defining `macro_rules!` macros that have proper visibility
2//! and scoping.
3//!
4//! The default scoping and publicity rules of `macro_rules!` macros are arcane and confusing:
5//! they behave like no other item in the Rust programming language,
6//! and introduce several frustrating limitations that are hard to work around.
7//! This problem will be eventually fixed by a new kind of macro known as [declarative macros 2.0],
8//! but that feature has been stuck in limbo for several years now
9//! and likely won't be seen on stable Rust for several years more.
10//!
11//! So that's where this crate comes in.
12//! It allows you to place `#[macro_vis]` or `#[macro_vis(VISIBILITY)]` on any `macro_rules!` macro
13//! and have it be treated exactly like any other item that supports a visibility modifier -
14//! structs, enums, functions, et cetera.
15//! It works with both crate-local and public macros,
16//! effectively superseding both `#[macro_use]` and `#[macro_export]`.
17//!
18//! See [the documentation of `#[macro_vis]`][attribute doc] for examples and usage.
19//!
20//! # The `uncommon_codepoints` warning
21//!
22//! You will get the `uncommon_codepoints` warning if you use this library,
23//! so you will probably want to place this in your crate root:
24//!
25//! ```
26//! #![allow(uncommon_codepoints)]
27//! ```
28//!
29//! # Documenting public macros
30//!
31//! The documentation of public macros can be slightly improved if run on a Nightly compiler.
32//! To enable this, you must first add this attribute to your crate root:
33//!
34//! ```
35//! #![cfg_attr(doc_nightly, feature(decl_macro, rustc_attrs))]
36//! ```
37//!
38//! Then you can build with the `doc_nightly` cfg set,
39//! either locally with `RUSTDOCFLAGS="--cfg doc_nightly" cargo +nightly doc`
40//! or on docs.rs by adding this to your `Cargo.toml`:
41//!
42//! ```toml
43//! [package.metadata.docs.rs]
44//! rustdoc-args = ["--cfg", "doc_nightly"]
45//! ```
46//!
47//! # How it works
48//!
49//! The trick to get non-`pub` macros working is simple;
50//! we just `use` the macro after its definition to get it to be treated like an item.
51//! The original macro is renamed to a randomly-generated identifier
52//! so it can't be accessed by regular code.
53//! This code:
54//!
55//! ```
56//! # use macro_vis::macro_vis;
57//! #[macro_vis(pub(crate))]
58//! macro_rules! example_macro { () => {}; }
59//! ```
60//!
61//! Gets expanded to something like:
62//!
63//! ```
64//! macro_rules! __example_macro_2994407750278293171 { () => {}; }
65//! pub(crate) use __example_macro_2994407750278293171 as example_macro;
66//! ```
67//!
68//! `pub` macros work the same, but apply `#[macro_export]` to the macro and ensure it doesn't show
69//! up in the documentation:
70//!
71//! ```
72//! #[doc(hidden)]
73//! #[macro_export]
74//! macro_rules! __example_macro_2994407750278293171 { () => {}; }
75//! pub use __example_macro_2994407750278293171 as example_macro;
76//! ```
77//!
78//! But because a re-export of a `#[doc(hidden)]` item is itself `#[doc(hidden)]`,
79//! the macro doesn't show up in the documentation at all.
80//! To solve this, the library employs two solutions depending on whether Nightly is available or
81//! not:
82//!
83//! - When `doc_nightly` is not enabled,
84//! the library emits a public function whose name is the macro name
85//! concatenated with LATIN LETTER RETROFLEX CLICK (ǃ),
86//! a character that looks nearly identical to the exclamation mark used to invoke macros.
87//! This is done to avoid name collisions between other functions of the same name
88//! and the macro's documentation double.
89//! However,
90//! it has the flaw of causing the macro to appear as a function in the docs even though it isn't,
91//! and it doesn't work well with re-exports.
92//!
93//! - When `doc_nightly` _is_ enabled,
94//! the library instead changes the macro to be a macros 2.0-style `pub macro`
95//! which obeys proper visibility rules by default.
96//! Unlike the previous solution,
97//! this one displays the macro in the correct documentation section and
98//! with the correct text color,
99//! as well as it working properly with inlined re-exports.
100//! The drawback is that it doesn't work on stable,
101//! and so has a greater risk of breaking in future.
102//!
103//! # MSRV
104//!
105//! This crate's minimum supported Rust version is 1.53,
106//! the first version to stabilize `non_ascii_idents`.
107//! It is currently considered a breaking change to increase this.
108//!
109//! # Credit
110//!
111//! Most of the ideas in this crate were discovered and shown to me by
112//! [Daniel Henry-Mantilla](https://github.com/danielhenrymantilla),
113//! so much of the credit goes to them.
114//!
115//! [attribute doc]: https://docs.rs/macro-vis/latest/macro_vis/attr.macro_vis.html
116//! [declarative macros 2.0]: https://github.com/rust-lang/rust/issues/39412
117
118use proc_macro::TokenStream as TokenStream1;
119use proc_macro2::{Delimiter, Group, Ident, Punct, Span, TokenStream, TokenTree};
120use quote::{format_ident, quote, quote_spanned};
121use std::{
122    collections::hash_map::RandomState,
123    convert::identity,
124    hash::{BuildHasher, Hasher},
125};
126
127/// Attribute that applies a visibility to a `macro_rules!` macro.
128///
129/// By default, placing `#[macro_vis]` on a macro causes it to have a private visibility
130/// like functions, structs and enums do by default.
131/// In comparison to regular `macro_rules!` macros, this means two things:
132/// - It can be used before it is declared.
133/// - It can't be used in submodules declared after the macro without importing it first.
134///
135/// ```
136/// # use macro_vis::macro_vis;
137/// // Use before declaration:
138/// private_macro!();
139///
140/// #[macro_vis]
141/// macro_rules! private_macro { () => {}; }
142///
143/// mod inner {
144///     // Doesn't work like with a regular macro, because it's not in scope:
145///     // private_macro!();
146///     // Does work:
147///     super::private_macro!();
148///     // or:
149///     crate::private_macro!();
150///     // You can also `use crate::private_macro;` just like any other item.
151/// }
152/// # // Force the code to be placed at the crate root
153/// # fn main() {}
154/// ```
155///
156/// You can also supply a custom visibility to `#[macro_vis]`.
157/// For example, to make a macro visible anywhere within the current crate:
158///
159/// ```
160/// inner::example_macro!();
161///
162/// // No `#[macro_use]` needed!
163/// mod inner {
164///     # use macro_vis::macro_vis;
165///     #[macro_vis(pub(crate))]
166///     macro_rules! example_macro { () => {}; }
167/// }
168/// ```
169///
170/// Public macros will be exported at the current module path
171/// instead of at the crate root as with `#[macro_export]`:
172///
173/// ```
174/// # #![allow(uncommon_codepoints)]
175/// pub mod inner {
176///     # use macro_vis::macro_vis;
177///     #[macro_vis(pub)]
178///     macro_rules! public_macro { () => {}; }
179/// }
180///
181/// // Doesn't work like with a `#[macro_export]`ed macro:
182/// // crate::public_macro!();
183///
184/// // Does work:
185/// crate::inner::public_macro!();
186/// # // Force the code to be placed at the crate root
187/// # fn main() {}
188/// ```
189#[proc_macro_attribute]
190pub fn macro_vis(attr: TokenStream1, item: TokenStream1) -> TokenStream1 {
191    macro_vis_inner(attr.into(), item.into())
192        .unwrap_or_else(identity)
193        .into()
194}
195
196fn macro_vis_inner(attr: TokenStream, item: TokenStream) -> Result<TokenStream, TokenStream> {
197    let vis = parse_vis(attr)?;
198    let Macro {
199        attrs,
200        macro_rules,
201        bang,
202        name,
203        arms,
204        rules,
205        semi,
206    } = parse_macro(item)?;
207
208    let real_name = format_ident!("__{}_{}", name, RandomState::new().build_hasher().finish());
209
210    Ok(match vis {
211        Vis::Local { pub_token, scope } => {
212            quote! {
213                #attrs
214                #macro_rules #bang #real_name #arms #semi
215                #pub_token #scope use #real_name as #name;
216            }
217        }
218        Vis::Public { pub_token } => {
219            let macro_token = Ident::new("macro", macro_rules.span());
220            let mut arms_2_0 = Group::new(Delimiter::Brace, macro_2_0_arms(&rules));
221            arms_2_0.set_span(arms.span());
222
223            // Concatenate it with LATIN LETTER RETROFLEX CLICK which is valid in identifers
224            let display_name = format_ident!("{}ǃ", name);
225
226            quote! {
227                #[cfg(not(doc_nightly))]
228                #[doc(hidden)]
229                #[macro_export]
230                #macro_rules #bang #real_name #arms #semi
231
232                #[cfg(not(doc_nightly))]
233                #[doc(hidden)]
234                #pub_token use #real_name as #name;
235
236                #[cfg(all(doc, not(doc_nightly)))]
237                #[doc = "<sup>**\\[macro\\]**</sup>"]
238                #attrs
239                #pub_token fn #display_name() {}
240
241                #[cfg(doc_nightly)]
242                // Force the macro to use `macro_rules!`-like mixed-site hygiene instead of the
243                // default definition-site hygiene.
244                #[rustc_macro_transparency = "semitransparent"]
245                #attrs
246                #pub_token #macro_token #name #arms_2_0
247            }
248        }
249    })
250}
251
252#[derive(Debug)]
253enum Vis {
254    Public {
255        pub_token: Ident,
256    },
257    Local {
258        pub_token: Option<Ident>,
259        scope: Option<Group>,
260    },
261}
262
263#[derive(Debug)]
264struct Macro {
265    attrs: TokenStream,
266    macro_rules: Ident,
267    bang: Punct,
268    name: Ident,
269    arms: Group,
270    rules: Vec<MacroRule>,
271    semi: Option<Punct>,
272}
273
274#[derive(Debug)]
275struct MacroRule {
276    matcher: Group,
277    equals: Punct,
278    greater_than: Punct,
279    transcriber: Group,
280    semi: Option<Punct>,
281}
282
283fn parse_vis(vis: TokenStream) -> Result<Vis, TokenStream> {
284    let mut vis = vis.into_iter();
285
286    let pub_token = match vis.next() {
287        Some(TokenTree::Ident(pub_token)) if pub_token == "pub" => pub_token,
288        Some(token) => {
289            return Err(error(token.span(), "expected visibility"));
290        }
291        None => {
292            return Ok(Vis::Local {
293                pub_token: None,
294                scope: None,
295            })
296        }
297    };
298
299    let scope = match vis.next() {
300        Some(TokenTree::Group(scope)) if scope.delimiter() == Delimiter::Parenthesis => scope,
301        Some(token) => {
302            return Err(error(token.span(), "expected parenthesis"));
303        }
304        None => return Ok(Vis::Public { pub_token }),
305    };
306
307    if let Some(trailing) = vis.next() {
308        return Err(error(trailing.span(), "trailing tokens"));
309    }
310
311    Ok(Vis::Local {
312        pub_token: Some(pub_token),
313        scope: Some(scope),
314    })
315}
316
317fn parse_macro(item: TokenStream) -> Result<Macro, TokenStream> {
318    let mut item = item.into_iter();
319
320    let mut attrs = TokenStream::new();
321
322    let macro_rules = loop {
323        match item.next() {
324            Some(TokenTree::Punct(punct)) if punct.as_char() == '#' => {
325                let next = item.next().expect("unexpected EOF in attribute");
326                if !matches!(&next, TokenTree::Group(group) if group.delimiter() == Delimiter::Bracket)
327                {
328                    unreachable!("attribute without square brackets");
329                }
330                attrs.extend([TokenTree::Punct(punct), next]);
331            }
332            Some(TokenTree::Ident(macro_rules)) if macro_rules == "macro_rules" => {
333                break macro_rules;
334            }
335            token => {
336                return Err(error(opt_span(&token), "expected macro_rules! macro"));
337            }
338        }
339    };
340
341    let bang = match item.next() {
342        Some(TokenTree::Punct(p)) if p.as_char() == '!' => p,
343        token => {
344            return Err(error(opt_span(&token), "expected exclamation mark"));
345        }
346    };
347
348    let name = match item.next() {
349        Some(TokenTree::Ident(ident)) => ident,
350        token => {
351            return Err(error(opt_span(&token), "expected identifier"));
352        }
353    };
354
355    let arms = match item.next() {
356        Some(TokenTree::Group(group)) => group,
357        token => {
358            return Err(error(opt_span(&token), "expected macro arms"));
359        }
360    };
361
362    let mut rule_tokens = arms.stream().into_iter();
363    let mut rules = Vec::new();
364
365    // The macro arms need proper validation because the following is actually accepted by rustc:
366    // #[macro_vis]
367    // macro_rules! some_macro { invalid tokens }
368    loop {
369        let matcher = match rule_tokens.next() {
370            Some(TokenTree::Group(group)) => group,
371            Some(token) => {
372                return Err(error(token.span(), "expected macro matcher"));
373            }
374            None if rules.is_empty() => {
375                return Err(error(arms.span(), "expected macro rules"));
376            }
377            None => break,
378        };
379        let equals = match rule_tokens.next() {
380            Some(TokenTree::Punct(equals)) if equals.as_char() == '=' => equals,
381            token => return Err(error(opt_span(&token), "expected =>")),
382        };
383        let greater_than = match rule_tokens.next() {
384            Some(TokenTree::Punct(greater_than)) if greater_than.as_char() == '>' => greater_than,
385            _ => return Err(error(equals.span(), "expected =>")),
386        };
387        let transcriber = match rule_tokens.next() {
388            Some(TokenTree::Group(group)) => group,
389            token => return Err(error(opt_span(&token), "expected macro transcriber")),
390        };
391        let mut rule = MacroRule {
392            matcher,
393            equals,
394            greater_than,
395            transcriber,
396            semi: None,
397        };
398        match rule_tokens.next() {
399            Some(TokenTree::Punct(semi)) if semi.as_char() == ';' => {
400                rule.semi = Some(semi);
401                rules.push(rule);
402            }
403            None => {
404                rules.push(rule);
405                break;
406            }
407            Some(token) => {
408                return Err(error(token.span(), "expected semicolon"));
409            }
410        }
411    }
412
413    let semi = if arms.delimiter() != Delimiter::Brace {
414        Some(match item.next() {
415            Some(TokenTree::Punct(semi)) if semi.as_char() == ';' => semi,
416            _ => unreachable!("no semicolon after () or []-delimited macro"),
417        })
418    } else {
419        None
420    };
421
422    if item.next().is_some() {
423        unreachable!("trailing tokens after macro_rules! macro");
424    }
425
426    Ok(Macro {
427        attrs,
428        macro_rules,
429        bang,
430        name,
431        arms,
432        rules,
433        semi,
434    })
435}
436
437fn opt_span(token: &Option<TokenTree>) -> Span {
438    token
439        .as_ref()
440        .map(|token| token.span())
441        .unwrap_or_else(Span::call_site)
442}
443
444fn macro_2_0_arms(rules: &[MacroRule]) -> TokenStream {
445    rules
446        .iter()
447        .map(
448            |MacroRule {
449                 matcher,
450                 equals,
451                 greater_than,
452                 transcriber,
453                 semi,
454             }| {
455                let comma = semi.as_ref().map(|semi| {
456                    let mut comma = Punct::new(',', semi.spacing());
457                    comma.set_span(semi.span());
458                    comma
459                });
460                quote!(#matcher #equals #greater_than #transcriber #comma)
461            },
462        )
463        .collect()
464}
465
466fn error(span: Span, msg: &str) -> TokenStream {
467    quote_spanned!(span=> ::core::compile_error!(#msg))
468}
469
470#[cfg(test)]
471mod tests {
472    use crate::{parse_macro, parse_vis, Macro, Vis};
473    use proc_macro2::TokenStream;
474    use quote::quote;
475
476    #[test]
477    fn vis_parse() {
478        assert!(matches!(
479            parse_vis(TokenStream::new()),
480            Ok(Vis::Local {
481                pub_token: None,
482                scope: None
483            })
484        ));
485        assert!(matches!(
486            parse_vis(quote!(pub)),
487            Ok(Vis::Public { pub_token }) if pub_token == "pub"
488        ));
489        assert!(matches!(
490            parse_vis(quote!(pub(crate))),
491            Ok(Vis::Local { pub_token: Some(pub_token), scope: Some(scope) })
492            if pub_token == "pub" && scope.to_string() == quote!((crate)).to_string()
493        ));
494        assert!(matches!(
495            parse_vis(quote!(pub(foo bar))),
496            Ok(Vis::Local { pub_token: Some(pub_token), scope: Some(scope) })
497            if pub_token == "pub" && scope.to_string() == quote!((foo bar)).to_string()
498        ));
499    }
500
501    #[test]
502    fn vis_error() {
503        macro_rules! assert_err {
504            (($($input:tt)*) -> $e:literal) => {
505                assert_eq!(
506                    parse_vis(quote!($($input)*)).unwrap_err().to_string(),
507                    quote!(::core::compile_error!($e)).to_string(),
508                );
509            };
510        }
511        assert_err!((priv) -> "expected visibility");
512        assert_err!((pub[crate]) -> "expected parenthesis");
513        assert_err!((pub() trailing) -> "trailing tokens");
514    }
515
516    #[test]
517    fn macro_parse() {
518        assert!(matches!(
519            parse_macro(quote!(macro_rules! foo { (m) => { t } })),
520            Ok(Macro { attrs, macro_rules, bang, name, arms, rules, semi: None })
521            if attrs.is_empty()
522                && macro_rules == "macro_rules"
523                && bang.as_char() == '!'
524                && name == "foo"
525                && arms.to_string() == quote!({ (m) => { t } }).to_string()
526                && rules.len() == 1
527                && rules[0].matcher.to_string() == quote!((m)).to_string()
528                && rules[0].equals.as_char() == '='
529                && rules[0].greater_than.as_char() == '>'
530                && rules[0].transcriber.to_string() == quote!({ t }).to_string()
531                && rules[0].semi.is_none()
532        ));
533        assert!(matches!(
534            parse_macro(quote! {
535                #[attr1]
536                #[attr2 = "foo"]
537                macro_rules! foo [
538                    {} => ();
539                    [$] => [[]];
540                ];
541            }),
542            Ok(Macro { attrs, arms, rules, semi: Some(semi), .. })
543            if attrs.to_string() == quote!(#[attr1] #[attr2 = "foo"]).to_string()
544                && arms.to_string() == quote!([{} => (); [$] => [[]];]).to_string()
545                && semi.as_char() == ';'
546                && rules.len() == 2
547                && rules[0].matcher.to_string() == quote!({}).to_string()
548                && rules[0].transcriber.to_string() == quote!(()).to_string()
549                && rules[0].semi.as_ref().map_or(false, |semi| semi.as_char() == ';')
550                && rules[1].matcher.to_string() == quote!([$]).to_string()
551                && rules[1].transcriber.to_string() == quote!([[]]).to_string()
552                && rules[1].semi.as_ref().map_or(false, |semi| semi.as_char() == ';')
553        ));
554    }
555
556    #[test]
557    fn macro_error() {
558        macro_rules! assert_err {
559            (($($input:tt)*) -> $e:literal) => {
560                assert_eq!(
561                    parse_macro(quote!($($input)*)).unwrap_err().to_string(),
562                    quote!(::core::compile_error!($e)).to_string(),
563                );
564            }
565        }
566        assert_err!(() -> "expected macro_rules! macro");
567        assert_err!((const _: () = {};) -> "expected macro_rules! macro");
568        assert_err!((macro_rules x {}) -> "expected exclamation mark");
569        assert_err!((macro_rules! { () => {} }) -> "expected identifier");
570        assert_err!((macro_rules! foo) -> "expected macro arms");
571        assert_err!((macro_rules! foo { }) -> "expected macro rules");
572        assert_err!((macro_rules! foo { # }) -> "expected macro matcher");
573        assert_err!((macro_rules! foo { () }) -> "expected =>");
574        assert_err!((macro_rules! foo { () = }) -> "expected =>");
575        assert_err!((macro_rules! foo { () => }) -> "expected macro transcriber");
576        assert_err!((macro_rules! foo { () => {} () => {} }) -> "expected semicolon");
577    }
578}