facet_macros_emit/
generics.rs

1use facet_macros_parse::{GenericParam, GenericParams, ToTokens, TokenStream};
2use quote::quote;
3
4use crate::LifetimeName;
5
6/// The name of a generic parameter
7#[derive(Clone)]
8pub enum GenericParamName {
9    /// "a" but formatted as "'a"
10    Lifetime(LifetimeName),
11
12    /// "T", formatted as "T"
13    Type(TokenStream),
14
15    /// "N", formatted as "N"
16    Const(TokenStream),
17}
18
19/// The name of a generic parameter with bounds
20#[derive(Clone)]
21pub struct BoundedGenericParam {
22    /// the parameter name
23    pub param: GenericParamName,
24
25    /// bounds like `'static`, or `Send + Sync`, etc. — None if no bounds
26    pub bounds: Option<TokenStream>,
27}
28
29/// Stores different representations of generic parameters for implementing traits.
30///
31/// This structure separates generic parameters into different formats needed when
32/// generating trait implementations.
33#[derive(Clone)]
34pub struct BoundedGenericParams {
35    /// Collection of generic parameters with their bounds
36    pub params: Vec<BoundedGenericParam>,
37}
38
39/// Display wrapper that shows generic parameters with their bounds
40///
41/// This is used to format generic parameters for display purposes,
42/// including both the parameter names and their bounds (if any).
43///
44/// # Example
45///
46/// For a parameter like `T: Clone`, this will display `<T: Clone>`.
47pub struct WithBounds<'a>(&'a BoundedGenericParams);
48
49/// Display wrapper that shows generic parameters without their bounds
50///
51/// This is used to format just the parameter names for display purposes,
52/// omitting any bounds information.
53///
54/// # Example
55///
56/// For a parameter like `T: Clone`, this will display just `<T>`.
57pub struct WithoutBounds<'a>(&'a BoundedGenericParams);
58
59/// Display wrapper that outputs generic parameters as a PhantomData
60///
61/// This is used to format generic parameters as a PhantomData type
62/// for use in trait implementations.
63///
64/// # Example
65///
66/// For parameters `<'a, T, const N: usize>`, this will display
67/// `::core::marker::PhantomData<(*mut &'__facet (), T, [u32; N])>`.
68pub struct AsPhantomData<'a>(&'a BoundedGenericParams);
69
70impl quote::ToTokens for AsPhantomData<'_> {
71    fn to_tokens(&self, tokens: &mut TokenStream) {
72        let mut temp = TokenStream::new();
73
74        {
75            #[expect(unused)]
76            let tokens = ();
77
78            // Track if we've written anything to handle commas correctly
79            let mut first_param = true;
80
81            // Generate all parameters in the tuple
82            for param in &self.0.params {
83                if !first_param {
84                    temp.extend(quote! { , });
85                }
86
87                match &param.param {
88                    GenericParamName::Lifetime(name) => {
89                        temp.extend(quote! { *mut &#name () });
90                    }
91                    GenericParamName::Type(name) => {
92                        temp.extend(quote! { #name });
93                    }
94                    GenericParamName::Const(name) => {
95                        temp.extend(quote! { [u32; #name] });
96                    }
97                }
98
99                first_param = false;
100            }
101
102            // If no parameters at all, add a unit type to make the PhantomData valid
103            if first_param {
104                temp.extend(quote! { () });
105            }
106        }
107        tokens.extend(quote! {
108            ::core::marker::PhantomData<(#temp)>
109        })
110    }
111}
112
113impl BoundedGenericParams {
114    pub fn as_phantom_data(&self) -> AsPhantomData<'_> {
115        AsPhantomData(self)
116    }
117}
118
119impl quote::ToTokens for WithBounds<'_> {
120    fn to_tokens(&self, tokens: &mut TokenStream) {
121        if self.0.params.is_empty() {
122            return;
123        }
124
125        tokens.extend(quote! {
126            <
127        });
128
129        for (i, param) in self.0.params.iter().enumerate() {
130            if i > 0 {
131                tokens.extend(quote! { , });
132            }
133
134            match &param.param {
135                GenericParamName::Lifetime(name) => {
136                    tokens.extend(quote! { #name });
137                }
138                GenericParamName::Type(name) => {
139                    tokens.extend(quote! { #name });
140                }
141                GenericParamName::Const(name) => {
142                    tokens.extend(quote! { const #name });
143                }
144            }
145
146            // Add bounds if they exist
147            if let Some(bounds) = &param.bounds {
148                tokens.extend(quote! { : #bounds });
149            }
150        }
151
152        tokens.extend(quote! {
153            >
154        });
155    }
156}
157
158impl quote::ToTokens for WithoutBounds<'_> {
159    fn to_tokens(&self, tokens: &mut TokenStream) {
160        if self.0.params.is_empty() {
161            return;
162        }
163
164        tokens.extend(quote! {
165            <
166        });
167
168        for (i, param) in self.0.params.iter().enumerate() {
169            if i > 0 {
170                tokens.extend(quote! { , });
171            }
172
173            match &param.param {
174                GenericParamName::Lifetime(name) => {
175                    tokens.extend(quote! { #name });
176                }
177                GenericParamName::Type(name) => {
178                    tokens.extend(quote! { #name });
179                }
180                GenericParamName::Const(name) => {
181                    tokens.extend(quote! { #name });
182                }
183            }
184        }
185
186        tokens.extend(quote! {
187            >
188        });
189    }
190}
191
192impl BoundedGenericParams {
193    pub fn display_with_bounds(&self) -> WithBounds<'_> {
194        WithBounds(self)
195    }
196
197    pub fn display_without_bounds(&self) -> WithoutBounds<'_> {
198        WithoutBounds(self)
199    }
200
201    /// Returns a display wrapper that formats generic parameters as a PhantomData
202    ///
203    /// This is a convenience method for generating PhantomData expressions
204    /// for use in trait implementations.
205    ///
206    /// # Example
207    ///
208    /// For generic parameters `<'a, T, const N: usize>`, this returns a wrapper that
209    /// when displayed produces:
210    /// `::core::marker::PhantomData<(*mut &'a (), T, [u32; N])>`
211    pub fn display_as_phantom_data(&self) -> AsPhantomData<'_> {
212        AsPhantomData(self)
213    }
214
215    pub fn with(&self, param: BoundedGenericParam) -> Self {
216        let mut params = self.params.clone();
217
218        match &param.param {
219            GenericParamName::Lifetime(_) => {
220                // Find the position after the last lifetime parameter
221                let insert_position = params
222                    .iter()
223                    .position(|p| !matches!(p.param, GenericParamName::Lifetime(_)))
224                    .unwrap_or(params.len());
225
226                params.insert(insert_position, param);
227            }
228            GenericParamName::Type(_) => {
229                // Find the position after the last type parameter but before any const parameters
230                let after_lifetimes = params
231                    .iter()
232                    .position(|p| !matches!(p.param, GenericParamName::Lifetime(_)))
233                    .unwrap_or(params.len());
234
235                let insert_position = params[after_lifetimes..]
236                    .iter()
237                    .position(|p| matches!(p.param, GenericParamName::Const(_)))
238                    .map(|pos| pos + after_lifetimes)
239                    .unwrap_or(params.len());
240
241                params.insert(insert_position, param);
242            }
243            GenericParamName::Const(_) => {
244                // Constants go at the end
245                params.push(param);
246            }
247        }
248
249        Self { params }
250    }
251
252    /// Adds a new lifetime parameter with the given name without bounds
253    ///
254    /// This is a convenience method for adding a lifetime parameter
255    /// that's commonly used in trait implementations.
256    pub fn with_lifetime(&self, name: LifetimeName) -> Self {
257        self.with(BoundedGenericParam {
258            param: GenericParamName::Lifetime(name),
259            bounds: None,
260        })
261    }
262
263    /// Adds a new type parameter with the given name without bounds
264    ///
265    /// This is a convenience method for adding a type parameter
266    /// that's commonly used in trait implementations.
267    pub fn with_type(&self, name: TokenStream) -> Self {
268        self.with(BoundedGenericParam {
269            param: GenericParamName::Type(name),
270            bounds: None,
271        })
272    }
273}
274
275impl BoundedGenericParams {
276    /// Parses generic parameters into separate components for implementing traits.
277    ///
278    /// This method takes a generic parameter declaration and populates the BoundedGenericParams struct
279    /// with different representations of the generic parameters needed for code generation.
280    ///
281    /// # Examples
282    ///
283    /// For a type like `struct Example<T: Clone, 'a, const N: usize>`, this would populate:
284    /// params with entries for each parameter and their bounds.
285    pub fn parse(generics: Option<&GenericParams>) -> Self {
286        let Some(generics) = generics else {
287            return Self { params: Vec::new() };
288        };
289
290        let mut params = Vec::new();
291
292        for param in generics.params.iter() {
293            match &param.value {
294                GenericParam::Type {
295                    name,
296                    bounds,
297                    default: _,
298                } => {
299                    params.push(BoundedGenericParam {
300                        param: GenericParamName::Type(name.to_token_stream()),
301                        bounds: bounds
302                            .as_ref()
303                            .map(|bounds| bounds.second.to_token_stream()),
304                    });
305                }
306                GenericParam::Lifetime { name, bounds } => {
307                    params.push(BoundedGenericParam {
308                        param: GenericParamName::Lifetime(LifetimeName(name.name.clone())),
309                        bounds: bounds
310                            .as_ref()
311                            .map(|bounds| bounds.second.to_token_stream()),
312                    });
313                }
314                GenericParam::Const {
315                    _const: _,
316                    name,
317                    _colon: _,
318                    typ,
319                    default: _,
320                } => {
321                    params.push(BoundedGenericParam {
322                        param: GenericParamName::Const(name.to_token_stream()),
323                        bounds: Some(typ.to_token_stream()),
324                    });
325                }
326            }
327        }
328
329        Self { params }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::{BoundedGenericParam, BoundedGenericParams, GenericParamName};
336    use crate::LifetimeName;
337    use quote::{ToTokens as _, quote};
338
339    // Helper to render ToTokens implementors to string for comparison
340    fn render_to_string<T: quote::ToTokens>(t: T) -> String {
341        quote!(#t).to_string()
342    }
343
344    #[test]
345    fn test_empty_generic_params() {
346        let p = BoundedGenericParams { params: vec![] };
347        assert_eq!(render_to_string(p.display_with_bounds()), "");
348        assert_eq!(render_to_string(p.display_without_bounds()), "");
349    }
350
351    #[test]
352    fn print_multiple_generic_params() {
353        let p = BoundedGenericParams {
354            params: vec![
355                BoundedGenericParam {
356                    bounds: Some(quote! { 'static }),
357                    param: GenericParamName::Lifetime(LifetimeName(quote::format_ident!("a"))),
358                },
359                BoundedGenericParam {
360                    bounds: Some(quote! { Clone + Debug }),
361                    param: GenericParamName::Type(quote! { T }),
362                },
363                BoundedGenericParam {
364                    bounds: None,
365                    param: GenericParamName::Type(quote! { U }),
366                },
367                BoundedGenericParam {
368                    bounds: Some(quote! { usize }), // Const params bounds are types
369                    param: GenericParamName::Const(quote! { N }),
370                },
371            ],
372        };
373        // Check display with bounds
374        let expected_with_bounds = quote! { <'a : 'static, T : Clone + Debug, U, const N : usize> };
375        assert_eq!(
376            p.display_with_bounds().to_token_stream().to_string(),
377            expected_with_bounds.to_string()
378        );
379
380        // Check display without bounds
381        let expected_without_bounds = quote! { <'a, T, U, N> }; // Note: const param N doesn't show `const` or type here
382        assert_eq!(
383            p.display_without_bounds().to_token_stream().to_string(),
384            expected_without_bounds.to_string()
385        );
386    }
387
388    #[test]
389    fn test_add_mixed_parameters() {
390        // Create a complex example with all parameter types
391        let mut params = BoundedGenericParams { params: vec![] };
392
393        // Add parameters in different order to test sorting
394        params = params.with(BoundedGenericParam {
395            bounds: None,
396            param: GenericParamName::Type(quote! { T }),
397        });
398
399        params = params.with(BoundedGenericParam {
400            bounds: Some(quote! { usize }), // Const bounds are types
401            param: GenericParamName::Const(quote! { N }),
402        });
403
404        params = params.with(BoundedGenericParam {
405            bounds: None,
406            param: GenericParamName::Lifetime(LifetimeName(quote::format_ident!("a"))),
407        });
408
409        params = params.with(BoundedGenericParam {
410            bounds: Some(quote! { Clone }),
411            param: GenericParamName::Type(quote! { U }),
412        });
413
414        params = params.with(BoundedGenericParam {
415            bounds: Some(quote! { 'static }),
416            param: GenericParamName::Lifetime(LifetimeName(quote::format_ident!("b"))),
417        });
418
419        params = params.with(BoundedGenericParam {
420            bounds: Some(quote! { u8 }), // Const bounds are types
421            param: GenericParamName::Const(quote! { M }),
422        });
423
424        // Expected order: lifetimes first, then types, then consts
425        let expected_without_bounds = quote! { <'a, 'b, T, U, N, M> };
426        // Compare string representations for robust assertion
427        assert_eq!(
428            params
429                .display_without_bounds()
430                .to_token_stream()
431                .to_string(),
432            expected_without_bounds.to_string()
433        );
434
435        let expected_with_bounds =
436            quote! { <'a, 'b : 'static, T, U : Clone, const N : usize, const M : u8> };
437        // Compare string representations for robust assertion
438        assert_eq!(
439            params.display_with_bounds().to_token_stream().to_string(),
440            expected_with_bounds.to_string()
441        );
442    }
443
444    #[test]
445    fn test_phantom_data_formatting() {
446        // Empty params should have PhantomData with a unit type
447        let empty = BoundedGenericParams { params: vec![] };
448        assert_eq!(
449            render_to_string(empty.display_as_phantom_data()),
450            ":: core :: marker :: PhantomData < (()) >"
451        );
452
453        // Single lifetime
454        let lifetime = BoundedGenericParams {
455            params: vec![BoundedGenericParam {
456                param: GenericParamName::Lifetime(LifetimeName(quote::format_ident!("a"))),
457                bounds: None,
458            }],
459        };
460        assert_eq!(
461            render_to_string(lifetime.display_as_phantom_data()),
462            ":: core :: marker :: PhantomData < (* mut & 'a ()) >"
463        );
464
465        // Single type
466        let type_param = BoundedGenericParams {
467            params: vec![BoundedGenericParam {
468                param: GenericParamName::Type(quote! { T }),
469                bounds: None,
470            }],
471        };
472        assert_eq!(
473            render_to_string(type_param.display_as_phantom_data()),
474            ":: core :: marker :: PhantomData < (T) >"
475        );
476
477        // Single const
478        let const_param = BoundedGenericParams {
479            params: vec![BoundedGenericParam {
480                param: GenericParamName::Const(quote! { N }),
481                bounds: None, // Bounds are irrelevant for PhantomData formatting
482            }],
483        };
484        assert_eq!(
485            render_to_string(const_param.display_as_phantom_data()),
486            ":: core :: marker :: PhantomData < ([u32 ; N]) >"
487        );
488
489        // Complex mix of params
490        let mixed = BoundedGenericParams {
491            params: vec![
492                BoundedGenericParam {
493                    param: GenericParamName::Lifetime(LifetimeName(quote::format_ident!("a"))),
494                    bounds: None,
495                },
496                BoundedGenericParam {
497                    param: GenericParamName::Type(quote! { T }),
498                    bounds: Some(quote! { Clone }), // Bounds irrelevant here
499                },
500                BoundedGenericParam {
501                    param: GenericParamName::Const(quote! { N }),
502                    bounds: Some(quote! { usize }), // Bounds irrelevant here
503                },
504            ],
505        };
506        let actual_tokens = mixed.display_as_phantom_data();
507        let expected_tokens = quote! {
508            ::core::marker::PhantomData<(*mut &'a (), T, [u32; N])>
509        };
510        assert_eq!(
511            actual_tokens.to_token_stream().to_string(),
512            expected_tokens.to_string()
513        );
514    }
515}