facet_macro_parse/
generic_params.rs

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