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}