Skip to main content

facet_macros_impl/
extension.rs

1//! Code generation for extension attributes.
2
3use crate::{Delimiter, Group, Ident, PFacetAttr, Punct, Spacing, TokenStream, TokenTree};
4use quote::{ToTokens, quote, quote_spanned};
5
6fn const_prefix_tokens(needs_const_dispatch: bool) -> TokenStream {
7    if needs_const_dispatch {
8        quote! { @const }
9    } else {
10        TokenStream::new()
11    }
12}
13
14fn emit_extension_attr_with_prefix(
15    ns_ident: &Ident,
16    key_ident: &impl ToTokens,
17    args: &TokenStream,
18    facet_crate: &TokenStream,
19    const_prefix: &TokenStream,
20) -> TokenStream {
21    if args.is_empty() {
22        quote! {
23            #facet_crate::__ext!(#const_prefix #ns_ident::#key_ident { })
24        }
25    } else {
26        quote! {
27            #facet_crate::__ext!(#const_prefix #ns_ident::#key_ident { | #args })
28        }
29    }
30}
31
32fn emit_extension_attr_for_field_with_prefix(
33    ns_ident: &Ident,
34    key_ident: &impl ToTokens,
35    args: &TokenStream,
36    field_name: &impl ToTokens,
37    field_type: &TokenStream,
38    facet_crate: &TokenStream,
39    const_prefix: &TokenStream,
40) -> TokenStream {
41    if args.is_empty() {
42        quote! {
43            #facet_crate::__ext!(#const_prefix #ns_ident::#key_ident { #field_name : #field_type })
44        }
45    } else {
46        quote! {
47            #facet_crate::__ext!(#const_prefix #ns_ident::#key_ident { #field_name : #field_type | #args })
48        }
49    }
50}
51
52fn emit_builtin_attr_with_prefix(
53    key: &impl ToTokens,
54    args: &TokenStream,
55    facet_crate: &TokenStream,
56    const_prefix: &TokenStream,
57) -> TokenStream {
58    if args.is_empty() {
59        quote! {
60            #facet_crate::__attr!(#const_prefix @ns { #facet_crate::builtin } #key { })
61        }
62    } else {
63        quote! {
64            #facet_crate::__attr!(#const_prefix @ns { #facet_crate::builtin } #key { | #args })
65        }
66    }
67}
68
69fn emit_builtin_attr_for_field_with_prefix(
70    key: &impl ToTokens,
71    args: &TokenStream,
72    field_name: &impl ToTokens,
73    field_type: &TokenStream,
74    facet_crate: &TokenStream,
75    const_prefix: &TokenStream,
76) -> TokenStream {
77    if args.is_empty() {
78        quote! {
79            #facet_crate::__attr!(#const_prefix @ns { #facet_crate::builtin } #key { #field_name : #field_type })
80        }
81    } else {
82        quote! {
83            #facet_crate::__attr!(#const_prefix @ns { #facet_crate::builtin } #key { #field_name : #field_type | #args })
84        }
85    }
86}
87
88/// Emits the code for an `ExtensionAttr` on a field.
89///
90/// This generates code that calls our `__ext!` proc macro, which then
91/// forwards to the extension crate's dispatcher macro with proper spans.
92///
93/// For `#[facet(xml::element)]` on field `server: Server`:
94/// ```ignore
95/// ::facet::__ext!(xml::element { server : Server })
96/// ```
97///
98/// For `#[facet(args::short = 'v')]` on field `verbose: bool`:
99/// ```ignore
100/// ::facet::__ext!(args::short { verbose : bool | = 'v' })
101/// ```
102pub fn emit_extension_attr_for_field(
103    ns_ident: &Ident,
104    key_ident: &impl ToTokens,
105    args: &TokenStream,
106    field_name: &impl ToTokens,
107    field_type: &TokenStream,
108    facet_crate: &TokenStream,
109    needs_const_dispatch: bool,
110) -> TokenStream {
111    let const_prefix = const_prefix_tokens(needs_const_dispatch);
112    emit_extension_attr_for_field_with_prefix(
113        ns_ident,
114        key_ident,
115        args,
116        field_name,
117        field_type,
118        facet_crate,
119        &const_prefix,
120    )
121}
122
123/// Emits the code for an `ExtensionAttr` without field context.
124///
125/// This is used for struct-level, enum-level, or variant-level attributes.
126///
127/// For `#[facet(ns::attr)]` at container level:
128/// ```ignore
129/// ::facet::__ext!(ns::attr { })
130/// ```
131///
132/// For `#[facet(ns::attr = "value")]` at container level:
133/// ```ignore
134/// ::facet::__ext!(ns::attr { | = "value" })
135/// ```
136pub fn emit_extension_attr(
137    ns_ident: &Ident,
138    key_ident: &impl ToTokens,
139    args: &TokenStream,
140    facet_crate: &TokenStream,
141    needs_const_dispatch: bool,
142) -> TokenStream {
143    let const_prefix = const_prefix_tokens(needs_const_dispatch);
144    emit_extension_attr_with_prefix(ns_ident, key_ident, args, facet_crate, &const_prefix)
145}
146
147/// Emits an attribute through grammar dispatch.
148///
149/// - Builtin attrs (no namespace) → `::facet::__attr!(...)`
150/// - Namespaced attrs → `::facet::__ext!(ns::key ...)`
151pub fn emit_attr(
152    attr: &PFacetAttr,
153    facet_crate: &TokenStream,
154    needs_const_dispatch: bool,
155) -> TokenStream {
156    let key = &attr.key;
157    let args = &attr.args;
158
159    match &attr.ns {
160        Some(ns) => {
161            // Namespaced: use __ext! which routes to ns::__attr!
162            emit_extension_attr(ns, key, args, facet_crate, needs_const_dispatch)
163        }
164        None => {
165            // Builtin: route directly to ::facet::__attr! (macro_export puts it at crate root)
166            let const_prefix = const_prefix_tokens(needs_const_dispatch);
167            emit_builtin_attr_with_prefix(key, args, facet_crate, &const_prefix)
168        }
169    }
170}
171
172/// Emits an attribute through grammar dispatch, with field context.
173///
174/// - Builtin attrs (no namespace) → `::facet::__attr!(...)`
175/// - Namespaced attrs → `::facet::__ext!(ns::key ...)`
176pub fn emit_attr_for_field(
177    attr: &PFacetAttr,
178    field_name: &impl ToTokens,
179    field_type: &TokenStream,
180    facet_crate: &TokenStream,
181    needs_const_dispatch: bool,
182) -> TokenStream {
183    let key = &attr.key;
184    let args = &attr.args;
185
186    match &attr.ns {
187        Some(ns) => {
188            // Namespaced: use existing helper
189            emit_extension_attr_for_field(
190                ns,
191                key,
192                args,
193                field_name,
194                field_type,
195                facet_crate,
196                needs_const_dispatch,
197            )
198        }
199        None => {
200            // Builtin: route directly to ::facet::__attr! (macro_export puts it at crate root)
201            let const_prefix = const_prefix_tokens(needs_const_dispatch);
202            emit_builtin_attr_for_field_with_prefix(
203                key,
204                args,
205                field_name,
206                field_type,
207                facet_crate,
208                &const_prefix,
209            )
210        }
211    }
212}
213
214/// Implementation of the `__ext!` proc macro.
215///
216/// This proc macro receives extension attribute invocations and forwards them
217/// to the extension crate's dispatcher macro while preserving spans for better
218/// error messages.
219///
220/// Input format: `ns::attr_name { field : Type }`, `@const ns::attr_name { ... }`, etc.
221/// Output: `ns::__attr!(@ns { ns } attr_name { ... })` (with optional `@const` prefix)
222pub fn ext_attr(input: TokenStream) -> TokenStream {
223    let mut tokens = input.into_iter().peekable();
224
225    // Optional mode marker: @const
226    let mut use_const_dispatch = false;
227    if let Some(TokenTree::Punct(p)) = tokens.peek()
228        && p.as_char() == '@'
229    {
230        tokens.next(); // '@'
231        match tokens.next() {
232            Some(TokenTree::Ident(mode)) if mode == "const" => {
233                use_const_dispatch = true;
234            }
235            _ => {
236                return quote! {
237                    ::core::compile_error!("__ext!: expected `const` after `@`")
238                };
239            }
240        }
241    }
242
243    // Parse: ns :: attr_name { ... }
244    let ns_ident = match tokens.next() {
245        Some(TokenTree::Ident(ident)) => ident,
246        _ => {
247            return quote! {
248                ::core::compile_error!("__ext!: expected namespace identifier")
249            };
250        }
251    };
252
253    // Expect ::
254    match (tokens.next(), tokens.next()) {
255        (Some(TokenTree::Punct(p1)), Some(TokenTree::Punct(p2)))
256            if p1.as_char() == ':' && p2.as_char() == ':' => {}
257        _ => {
258            return quote! {
259                ::core::compile_error!("__ext!: expected '::'")
260            };
261        }
262    }
263
264    // Get the attribute name (this has the span we want to preserve!)
265    let attr_ident = match tokens.next() {
266        Some(TokenTree::Ident(ident)) => ident,
267        _ => {
268            return quote! {
269                ::core::compile_error!("__ext!: expected attribute name")
270            };
271        }
272    };
273
274    // Get the braced content { ... }
275    let body = match tokens.next() {
276        Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => g,
277        _ => {
278            return quote! {
279                ::core::compile_error!("__ext!: expected braced body")
280            };
281        }
282    };
283
284    // Build the output: ns::__attr!(@ns { ns } attr_name { ... })
285    // The attr_ident preserves its original span!
286    // We pass the namespace so __attr! can do `use $ns::Attr as __ExtAttr;`
287    let __attr = Ident::new("__attr", attr_ident.span());
288    let at = Punct::new('@', Spacing::Alone);
289    let ns_keyword = Ident::new("ns", attr_ident.span());
290
291    let colon1 = Punct::new(':', Spacing::Joint);
292    let colon2 = Punct::new(':', Spacing::Alone);
293    let bang = Punct::new('!', Spacing::Alone);
294
295    // Build the macro invocation tokens manually to preserve spans
296    let mut output = TokenStream::new();
297    output.extend([TokenTree::Ident(ns_ident.clone())]);
298    output.extend([TokenTree::Punct(colon1.clone())]);
299    output.extend([TokenTree::Punct(colon2.clone())]);
300    output.extend([TokenTree::Ident(__attr)]);
301    output.extend([TokenTree::Punct(bang)]);
302
303    // Build the macro arguments: (@ns { ns_ident } attr_name { ... })
304    let mut macro_args = TokenStream::new();
305    if use_const_dispatch {
306        macro_args.extend([TokenTree::Punct(Punct::new('@', Spacing::Alone))]);
307        macro_args.extend([TokenTree::Ident(Ident::new("const", attr_ident.span()))]);
308    }
309    // @ns { ns_ident }
310    macro_args.extend([TokenTree::Punct(at)]);
311    macro_args.extend([TokenTree::Ident(ns_keyword)]);
312    let mut ns_group_content = TokenStream::new();
313    ns_group_content.extend([TokenTree::Ident(ns_ident)]);
314    macro_args.extend([TokenTree::Group(Group::new(
315        Delimiter::Brace,
316        ns_group_content,
317    ))]);
318    // attr_name { ... }
319    macro_args.extend([TokenTree::Ident(attr_ident)]);
320    macro_args.extend([TokenTree::Group(body)]);
321
322    let args_group = Group::new(Delimiter::Parenthesis, macro_args);
323    output.extend([TokenTree::Group(args_group)]);
324
325    output
326}
327
328/// Implementation of the `__unknown_attr!` proc macro.
329///
330/// This generates a compile_error! with the span pointing to the unknown identifier.
331///
332/// Input: `unknown_ident`
333/// Output: `compile_error!("unknown extension attribute `unknown_ident`")` with span on the ident
334pub fn unknown_attr(input: TokenStream) -> TokenStream {
335    let mut tokens = input.into_iter();
336
337    // Get the unknown attribute identifier
338    let ident = match tokens.next() {
339        Some(TokenTree::Ident(ident)) => ident,
340        _ => {
341            return quote! {
342                ::core::compile_error!("__unknown_attr!: expected identifier")
343            };
344        }
345    };
346
347    let span = ident.span();
348    let message = format!("unknown extension attribute `{ident}`");
349
350    quote_spanned! { span =>
351        ::core::compile_error!(#message)
352    }
353}
354
355/// Implementation of the `__no_args!` proc macro.
356///
357/// Generates a "does not accept arguments" error with the span pointing to the arguments.
358///
359/// Input: `"ns::attr", token`
360/// Output: `compile_error!("ns::attr does not accept arguments")` with span on token
361pub fn no_args(input: TokenStream) -> TokenStream {
362    let mut tokens = input.into_iter();
363
364    // Get the message string literal
365    let msg = match tokens.next() {
366        Some(TokenTree::Literal(lit)) => {
367            let s = lit.to_string();
368            s.trim_matches('"').to_string()
369        }
370        _ => {
371            return quote! {
372                ::core::compile_error!("__no_args!: expected string literal")
373            };
374        }
375    };
376
377    // Skip comma
378    tokens.next();
379
380    // Get token for span
381    let span = match tokens.next() {
382        Some(tt) => tt.span(),
383        None => {
384            return quote! {
385                ::core::compile_error!("__no_args!: expected token for span")
386            };
387        }
388    };
389
390    let message = format!("{msg} does not accept arguments");
391
392    quote_spanned! { span =>
393        ::core::compile_error!(#message)
394    }
395}