telety_impl/
command.rs

1use std::borrow::Cow;
2
3use proc_macro2::{Punct, Spacing, Span, TokenStream};
4use quote::{format_ident, quote, quote_spanned, ToTokens};
5use syn::{parse_quote, parse_quote_spanned, spanned::Spanned as _, Ident, LitInt, Path};
6
7use crate::{find_and_replace::SingleToken, Telety};
8
9pub(crate) type GenerateMacroTokens = fn(tele_ty: &Telety) -> Option<TokenStream>;
10
11/// Used to invoke the telety-generated macro in a manageable way.
12pub struct Command {
13    version: usize,
14    keyword: &'static str,
15    generate_macro_tokens: GenerateMacroTokens,
16}
17
18impl Command {
19    pub(crate) const fn new(
20        version: usize,
21        keyword: &'static str,
22        generate_macro_tokens: GenerateMacroTokens,
23    ) -> Self {
24        Self {
25            version,
26            keyword,
27            generate_macro_tokens,
28        }
29    }
30
31    pub(crate) const fn version(&self) -> usize {
32        self.version
33    }
34
35    fn version_lit(&self, span: Option<Span>) -> LitInt {
36        let span = span.unwrap_or(Span::call_site());
37        LitInt::new(&self.version().to_string(), span)
38    }
39
40    fn keyword(&self, span: Option<Span>) -> Ident {
41        let span = span.unwrap_or(Span::call_site());
42        Ident::new(self.keyword, span)
43    }
44
45    #[doc(hidden)]
46    pub fn generate_macro_arm(&self, ty: &Telety) -> syn::Result<Option<TokenStream>> {
47        if let Some(implementation) = (self.generate_macro_tokens)(ty) {
48            let span = ty.item().span();
49
50            let ParameterIdents {
51                args,
52                needle,
53                haystack,
54            } = ParameterIdents::new(span);
55
56            let keyword = self.keyword(Some(span));
57            let version = self.version_lit(Some(span));
58            Ok(Some(quote_spanned! { span =>
59                (#version, #keyword $( ( $($#args:tt)* ) )?, $#needle:tt, $($#haystack:tt)*) => {
60                    #implementation
61                };
62            }))
63        } else {
64            Ok(None)
65        }
66    }
67
68    /// Creates a macro invocation to use this command with the telety-generated macro at `macro_path`.  
69    /// The output of the command will be inserted into `haystack` at each instance of `needle`.
70    /// `macro_path` must point to a valid telety-generated macro, otherwise a compile error will occur.  
71    /// To support future [Command]s, `args` are passed to the command invocation, but they are not currently used.  
72    /// ## Example
73    /// ```rust,ignore
74    /// # use syn::parse2;
75    /// #[proc_macro]
76    /// pub fn my_public_macro(tokens: TokenStream) -> TokenStream {
77    ///     // ...
78    ///     let my_needle: TokenTree = format_ident!("__my_needle__").into();
79    ///     v1::UNIQUE_IDENT.apply(
80    ///         &parse_quote!(crate::MyTeletyObj),
81    ///         &my_needle,
82    ///         quote! {
83    ///             my_crate::my_macro_implementation!(#my_needle);
84    ///         },
85    ///         None,
86    ///     )
87    /// }
88    /// #[doc(hidden)]
89    /// #[proc_macro]
90    /// pub fn my_macro_implementation(tokens: TokenStream) -> TokenStream {
91    ///     let ident: Ident = parse2(tokens);
92    ///     // ...
93    /// }
94    /// ```
95    pub fn apply(
96        &'static self,
97        macro_path: Path,
98        needle: impl Into<SingleToken>,
99        haystack: impl ToTokens,
100    ) -> Apply {
101        Apply::new(
102            self,
103            macro_path,
104            needle.into(),
105            haystack.into_token_stream(),
106        )
107    }
108}
109
110pub(crate) struct ParameterIdents {
111    pub args: Ident,
112    pub needle: Ident,
113    pub haystack: Ident,
114}
115
116impl ParameterIdents {
117    pub fn new(span: Span) -> Self {
118        Self {
119            args: Ident::new("args", span),
120            needle: Ident::new("needle", span),
121            haystack: Ident::new("haystack", span),
122        }
123    }
124}
125
126/// Creates the [TokenStream] for the [Command] using the given arguments.  
127/// Can be interpolated directly in a [quote!] macro.
128pub struct Apply {
129    command: &'static Command,
130    macro_path: Path,
131    needle: SingleToken,
132    haystack: TokenStream,
133    args: Option<TokenStream>,
134    fallback: Option<TokenStream>,
135    telety_path: Option<Path>,
136    unique_macro_ident: Option<Ident>,
137}
138
139impl Apply {
140    fn new(
141        command: &'static Command,
142        macro_path: Path,
143        needle: SingleToken,
144        haystack: TokenStream,
145    ) -> Self {
146        Self {
147            command,
148            macro_path,
149            needle,
150            haystack,
151            args: None,
152            fallback: None,
153            telety_path: None,
154            unique_macro_ident: None,
155        }
156    }
157
158    #[doc(hidden)]
159    /// Pass arguments to the command invocation.  
160    /// Note: no commands currently use any arguments
161    pub fn with_arguments(mut self, arguments: impl ToTokens) -> Self {
162        self.args.replace(arguments.into_token_stream());
163        self
164    }
165
166    /// If `macro_path` does not contain a macro, instead expand to the `fallback` tokens.  
167    /// By default, the command or `fallback` will be expanded inside an anonymous block,
168    /// so any items cannot not be referenced from outside. Use [Apply::with_macro_forwarding]
169    /// to expand the output directly in this scope.
170    pub fn with_fallback(mut self, fallback: impl ToTokens) -> Self {
171        self.fallback.replace(fallback.into_token_stream());
172        self
173    }
174
175    /// Specify the location of the telety crate.  
176    /// This is only required if telety is not located at the default path `::telety`
177    /// and [Apply::with_fallback] is used.
178    pub fn with_telety_path(mut self, telety_path: Path) -> Self {
179        self.telety_path.replace(telety_path);
180        self
181    }
182
183    /// If a fallback is set, forward the final haystack/fallback tokens through a macro
184    /// so that they are evaluated without additional block scopes.  
185    /// This is usually required if you a creating a named item (such as a `struct` or `enum`), but
186    /// not for `impls`.
187    /// `unique_macro_ident` must be unique within the crate.  
188    /// This has no effect if [Apply::with_fallback] is not used.
189    pub fn with_macro_forwarding(mut self, unique_macro_ident: Ident) -> Self {
190        self.unique_macro_ident.replace(unique_macro_ident);
191        self
192    }
193}
194
195impl ToTokens for Apply {
196    fn to_tokens(&self, tokens: &mut TokenStream) {
197        let macro_path = &self.macro_path;
198        let needle = &self.needle;
199        let mut haystack = self.haystack.to_token_stream();
200        let args = self.args.as_ref().map(|ts| quote!((#ts)));
201
202        let span = self.haystack.span();
203        let version = self.command.version_lit(Some(span));
204        let keyword = self.command.keyword(Some(span));
205
206        let textual_macro_ident: Ident = format_ident!(
207            "my_macro_{}",
208            self.unique_macro_ident
209                .as_ref()
210                .unwrap_or(&format_ident!("a"))
211        );
212
213        let mut fallback = self.fallback.as_ref().map(|f| f.to_token_stream());
214
215        if let Some(fallback) = fallback.as_mut() {
216            if let Some(unique_macro_ident) = &self.unique_macro_ident {
217                let macro_wrapper = |contents: &TokenStream| {
218                    // Replace `$` in the original content with `$dollar dollar`
219                    // because we have 2 extra layers of macro rules indirection
220                    let contents = crate::find_and_replace::find_and_replace(
221                        Punct::new('$', Spacing::Alone),
222                        quote!($dollar dollar),
223                        contents.into_token_stream(),
224                    );
225
226                    quote_spanned! { span =>
227                        // Export a macro...
228                        #[doc(hidden)]
229                        #[macro_export]
230                        macro_rules! #unique_macro_ident {
231                            ($dollar:tt) => {
232                                // which defines a macro, ...
233                                macro_rules! #textual_macro_ident {
234                                    ($dollar dollar:tt) => {
235                                        // which expands to our actual contents
236                                        #contents
237                                    };
238                                }
239                            };
240                        }
241                    }
242                };
243
244                *fallback = macro_wrapper(fallback);
245                haystack = macro_wrapper(&haystack);
246            }
247        }
248
249        let mut output = parse_quote_spanned! { span =>
250            #macro_path! { #version, #keyword #args, #needle, #haystack }
251        };
252
253        if let Some(fallback) = fallback {
254            let telety_path = self
255                .telety_path
256                .as_ref()
257                .map(Cow::Borrowed)
258                .unwrap_or_else(|| Cow::Owned(parse_quote!(::telety)));
259
260            output = parse_quote_spanned! { span =>
261                #telety_path::util::try_invoke! {
262                    #output
263                    #fallback
264                }
265            };
266
267            if let Some(unique_macro_ident) = &self.unique_macro_ident {
268                let temp_ident = format_ident!("_{unique_macro_ident}");
269
270                // In order to invoke the macro from its export at the crate root, we need this trick:
271                // We must glob import the crate root, and invoke the macro without a path. To avoid polluting the current module,
272                // we make a sub-module, and invoke within there. Since we need to expand the caller's `haystack` in the main module,
273                // this macro is a layer of indirection which defines another macro. Name resolution does not like the main module
274                // peeking into our sub-module, so we will invoke our final macro using textual scope instead of module scope.
275                // #[macro_use] allows the macro to remain in textual scope after the sub-module, so we can invoke it there.
276                let import = quote_spanned! { span =>
277                    #[macro_use]
278                    #[doc(hidden)]
279                    mod #temp_ident {
280                        pub(super) use crate::*;
281
282                        #unique_macro_ident!($);
283                    }
284                };
285
286                let invoke = quote_spanned! { span =>
287                    #textual_macro_ident! { $ }
288                };
289
290                output = quote_spanned! { span =>
291                    #output
292                    #import
293                    #invoke
294                };
295            }
296        }
297
298        output.to_tokens(tokens);
299    }
300}