apply_macro/
lib.rs

1#![deny(
2    warnings,
3    missing_docs,
4    rustdoc::all,
5    clippy::pedantic,
6    clippy::dbg_macro,
7    clippy::semicolon_if_nothing_returned
8)]
9#![forbid(unsafe_code)]
10#![doc(test(attr(deny(warnings), forbid(unsafe_code))))]
11#![no_std]
12//! An attribute macro to apply function-like macros.
13//! It can apply *multiple* function-like macros that *only* accept an item (do
14//! *not* accept other function-like macro calls) to a single item or just
15//! improve the *readability* of the code.
16//!
17//! This crate has *no* dependency so you don't need to worry about compile
18//! time.
19//!
20//! # Examples
21//! ```
22//! use apply_macro::apply;
23//!
24//! macro_rules! derive_debug {
25//!     {
26//!         #[$attr:meta] // will receive `#[apply(derive_clone, derive_partial_eq)]`
27//!         $input:item
28//!     } => {
29//!         #[$attr]
30//!         #[derive(Debug)]
31//!         $input
32//!     };
33//! }
34//!
35//! macro_rules! derive_clone {
36//!     {
37//!         #[$attr:meta] // will receive `#[apply(derive_partial_eq)]`
38//!         $input:item
39//!     } => {
40//!         #[$attr]
41//!         #[derive(Clone)]
42//!         $input
43//!     };
44//! }
45//!
46//! macro_rules! derive_partial_eq {
47//!     ($input:item) => {
48//!         #[derive(PartialEq)]
49//!         $input
50//!     };
51//! }
52//!
53//! #[apply(derive_debug, derive_clone, derive_partial_eq)]
54//! struct Num(i32);
55//!
56//! assert_eq!(Num(-1).clone(), Num(-1));
57//! assert_ne!(Num(1), Num(-1));
58//!
59//! #[apply(derive_debug, derive_clone, derive_partial_eq,)]
60//! struct TrailingCommaIsAllowed;
61//!
62//! assert_eq!(TrailingCommaIsAllowed, TrailingCommaIsAllowed);
63//! ```
64//!
65//! Single macro (`thread_local!`) example:
66//! ```
67//! use apply_macro::apply;
68//! use std::cell::Cell;
69//!
70//! #[apply(thread_local)]
71//! static TLS: Cell<i32> = 1.into();
72//!
73//! TLS.with(|tls| assert_eq!(tls.replace(-1), 1));
74//! TLS.with(|tls| assert_eq!(tls.get(), -1));
75//! ```
76//!
77//! Empty argument is allowed (consistent with `#[derive()]`):
78//! ```
79//! use apply_macro::apply;
80//!
81//! #[apply()]
82//! #[derive()] // consistent
83//! # #[allow(unused_attributes, dead_code)]
84//! struct EmptyArg;
85//! ```
86//!
87//! Although, as a procedural macro, `#[apply]` can't be banned:
88//! ```
89//! # use apply_macro::apply;
90//! #[apply] // same as `#[apply()]`
91//! # #[allow(dead_code)]
92//! struct Oops;
93//! ```
94
95use core::iter::once;
96use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree};
97
98fn into_tt(tt: impl Into<TokenTree>) -> impl Iterator<Item = TokenTree> {
99    once(tt.into())
100}
101
102macro_rules! punct {
103    [$punct1:literal $($punct:literal)*] => {
104        into_tt(Punct::new($punct1, Spacing::Joint))
105            $(.chain(into_tt(Punct::new($punct, Spacing::Joint))))*
106    };
107}
108
109/// The main attribute macro of this crate.
110///
111/// This macro accepts comma-separated paths to the function-like macros you
112/// want to call as arguments. See also [examples in the crate-level
113/// documentation](crate#examples).
114///
115/// ## Limitation
116/// This macro does not validate its arguments:
117/// ```
118/// use apply_macro::apply;
119///
120/// macro_rules! derive_debug {
121///     ($input:item) => {
122///         #[derive(Debug)]
123///         $input
124///     };
125/// }
126///
127/// #[apply(#[derive(Debug)] struct AnotherStruct; derive_debug)]
128/// struct ImplsDebug;
129///
130/// dbg!(AnotherStruct, ImplsDebug);
131/// ```
132#[proc_macro_attribute]
133pub fn apply(args: TokenStream, input: TokenStream) -> TokenStream {
134    if args.is_empty() {
135        input
136    } else {
137        let mut args = args.into_iter();
138        let mut result = TokenStream::new();
139        for tt in &mut args {
140            if let TokenTree::Punct(ref punct) = tt {
141                if *punct == ',' {
142                    let args: TokenStream = args.collect();
143                    if args.is_empty() {
144                        break;
145                    }
146                    result.extend(
147                        punct!['!'].chain(into_tt(Group::new(
148                            Delimiter::Brace,
149                            punct!['#']
150                                .chain(into_tt(Group::new(
151                                    Delimiter::Bracket,
152                                    punct![':' ':']
153                                        .chain(into_tt(Ident::new(
154                                            "apply_macro",
155                                            Span::call_site(),
156                                        )))
157                                        .chain(punct![':' ':'])
158                                        .chain(into_tt(Ident::new("apply", Span::call_site())))
159                                        .chain(into_tt(Group::new(Delimiter::Parenthesis, args)))
160                                        .collect(),
161                                )))
162                                .chain(input)
163                                .collect(),
164                        ))),
165                    );
166                    return result;
167                }
168            }
169            result.extend(once(tt));
170        }
171        result.extend(punct!['!'].chain(into_tt(Group::new(Delimiter::Brace, input))));
172        result
173    }
174}