ts_rs_macros/
lib.rs

1#![macro_use]
2#![deny(unused)]
3
4use std::collections::{HashMap, HashSet};
5
6use proc_macro2::{Ident, TokenStream};
7use quote::{format_ident, quote};
8use syn::{
9    parse_quote, spanned::Spanned, ConstParam, Expr, GenericParam, Generics, Item, LifetimeParam,
10    Path, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
11    TypeTuple, WhereClause, WherePredicate,
12};
13
14use crate::{attr::Repr, deps::Dependencies, utils::format_generics};
15
16#[macro_use]
17mod utils;
18mod attr;
19mod deps;
20mod optional;
21mod types;
22
23struct DerivedTS {
24    crate_rename: Path,
25    ts_name: Expr,
26    docs: Vec<Expr>,
27    inline: TokenStream,
28    inline_flattened: Option<TokenStream>,
29    dependencies: Dependencies,
30    concrete: HashMap<Ident, Type>,
31    bound: Option<Vec<WherePredicate>>,
32    ts_enum: Option<Repr>,
33
34    export: bool,
35    export_to: Option<Expr>,
36}
37
38impl DerivedTS {
39    fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream {
40        let export = self
41            .export
42            .then(|| self.generate_export_test(&rust_ty, &generics));
43
44        let output_path_fn = {
45            let ts_name = &self.ts_name;
46            // expression of type `String` containing the file path
47            let path_string = match &self.export_to {
48                Some(dir_or_file) => quote![{
49                    let dir_or_file = format!("{}", #dir_or_file);
50                    if dir_or_file.ends_with('/') {
51                        // export into directory
52                        format!("{dir_or_file}{}.ts", #ts_name)
53                    } else {
54                        // export into provided file
55                        format!("{dir_or_file}")
56                    }
57                }],
58                None => quote![format!("{}.ts", #ts_name)],
59            };
60
61            quote! {
62                fn output_path() -> Option<std::path::PathBuf> {
63                    Some(std::path::PathBuf::from(#path_string))
64                }
65            }
66        };
67
68        let crate_rename = self.crate_rename.clone();
69        let docs = match &*self.docs {
70            [] => None,
71            docs => Some(quote! {
72                fn docs() -> Option<String> {
73                    Some(#crate_rename::format_docs(&[#(#docs),*]))
74                }
75            }),
76        };
77
78        let ident = self.ts_name.clone();
79        let impl_start = generate_impl_block_header(
80            &crate_rename,
81            &rust_ty,
82            &generics,
83            self.bound.as_deref(),
84            &self.dependencies,
85        );
86        let assoc_type = generate_assoc_type(&rust_ty, &crate_rename, &generics, &self.concrete);
87        let name = self.generate_name_fn(&generics);
88        let inline = self.generate_inline_fn();
89        let decl = self.generate_decl_fn(&rust_ty, &generics);
90        let dependencies = &self.dependencies;
91        let generics_fn = self.generate_generics_fn(&generics);
92
93        quote! {
94            #impl_start {
95                #assoc_type
96                type OptionInnerType = Self;
97
98                fn ident() -> String {
99                    (#ident).to_string()
100                }
101
102                #docs
103                #name
104                #decl
105                #inline
106                #generics_fn
107                #output_path_fn
108
109                fn visit_dependencies(v: &mut impl #crate_rename::TypeVisitor)
110                where
111                    Self: 'static,
112                {
113                    #dependencies
114                }
115            }
116
117            #export
118        }
119    }
120
121    /// Returns an expression which evaluates to the TypeScript name of the type, including generic
122    /// parameters.
123    fn name_with_generics(&self, generics: &Generics) -> TokenStream {
124        let name = &self.ts_name;
125        let crate_rename = &self.crate_rename;
126        let mut generics_ts_names = generics
127            .type_params()
128            .filter(|ty| !self.concrete.contains_key(&ty.ident))
129            .map(|ty| &ty.ident)
130            .map(|generic| quote!(<#generic as #crate_rename::TS>::name()))
131            .peekable();
132
133        if generics_ts_names.peek().is_some() {
134            quote! {
135                format!("{}<{}>", #name, vec![#(#generics_ts_names),*].join(", "))
136            }
137        } else {
138            quote!((#name).to_string())
139        }
140    }
141
142    /// Generate a dummy unit struct for every generic type parameter of this type.
143    /// # Example:
144    /// ```compile_fail
145    /// struct Generic<A, B, const C: usize> { /* ... */ }
146    /// ```
147    /// has two generic type parameters, `A` and `B`. This function will therefore generate
148    /// ```compile_fail
149    /// struct A;
150    /// impl ts_rs::TS for A { /* .. */ }
151    ///
152    /// struct B;
153    /// impl ts_rs::TS for B { /* .. */ }
154    /// ```
155    fn generate_generic_types(&self, generics: &Generics) -> TokenStream {
156        let crate_rename = &self.crate_rename;
157        let generics = generics
158            .type_params()
159            .filter(|ty| !self.concrete.contains_key(&ty.ident))
160            .map(|ty| ty.ident.clone());
161        let name = quote![<Self as #crate_rename::TS>::name()];
162        quote! {
163            #(
164                #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
165                struct #generics;
166                impl std::fmt::Display for #generics {
167                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168                        write!(f, "{:?}", self)
169                    }
170                }
171                impl #crate_rename::TS for #generics {
172                    type WithoutGenerics = #generics;
173                    type OptionInnerType = Self;
174                    fn name() -> String { stringify!(#generics).to_owned() }
175                    fn inline() -> String { panic!("{} cannot be inlined", #name) }
176                    fn inline_flattened() -> String { stringify!(#generics).to_owned() }
177                    fn decl() -> String { panic!("{} cannot be declared", #name) }
178                    fn decl_concrete() -> String { panic!("{} cannot be declared", #name) }
179                }
180            )*
181        }
182    }
183
184    fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
185        let test_fn = format_ident!(
186            "export_bindings_{}",
187            rust_ty.to_string().to_lowercase().replace("r#", "")
188        );
189        let crate_rename = &self.crate_rename;
190        let generic_params = generics
191            .type_params()
192            .map(|ty| match self.concrete.get(&ty.ident) {
193                None => quote! { #crate_rename::Dummy },
194                Some(ty) => quote! { #ty },
195            });
196        let ty = quote!(<#rust_ty<#(#generic_params),*> as #crate_rename::TS>);
197
198        quote! {
199            #[cfg(test)]
200            #[test]
201            fn #test_fn() {
202                #ty::export_all().expect("could not export type");
203            }
204        }
205    }
206
207    fn generate_generics_fn(&self, generics: &Generics) -> TokenStream {
208        let crate_rename = &self.crate_rename;
209        let generics = generics
210            .type_params()
211            .filter(|ty| !self.concrete.contains_key(&ty.ident))
212            .map(|TypeParam { ident, .. }| {
213                quote![
214                    v.visit::<#ident>();
215                    <#ident as #crate_rename::TS>::visit_generics(v);
216                ]
217            });
218        quote! {
219            fn visit_generics(v: &mut impl #crate_rename::TypeVisitor)
220            where
221                Self: 'static,
222            {
223                #(#generics)*
224            }
225        }
226    }
227
228    fn generate_name_fn(&self, generics: &Generics) -> TokenStream {
229        let name = self.name_with_generics(generics);
230        quote! {
231            fn name() -> String {
232                #name
233            }
234        }
235    }
236
237    fn generate_inline_fn(&self) -> TokenStream {
238        let inline = &self.inline;
239        let crate_rename = &self.crate_rename;
240
241        let inline_flattened = self.inline_flattened.clone().unwrap_or_else(|| {
242            quote! {
243                panic!("{} cannot be flattened", <Self as #crate_rename::TS>::name())
244            }
245        });
246
247        let inline = match self.ts_enum {
248            Some(Repr::Int) => quote! {
249                let variants = #inline.replace(|x: char| !x.is_numeric() && x != ',', "");
250                let mut variants = variants
251                    .split(',')
252                    .map(|x| isize::from_str_radix(x, 10).ok())
253                    .peekable();
254
255                if variants.peek().is_none() {
256                    return "never".into()
257                }
258
259                let mut buffer = String::new();
260                let mut latest = None::<isize>;
261
262                for variant in variants {
263                    let value = variant.or(latest.map(|x| x + 1)).unwrap_or(0);
264                    buffer.push_str(&format!("{} | ", value));
265
266                    latest = Some(value)
267                }
268
269                buffer.trim_end_matches(['|', ' ']).into()
270            },
271            Some(Repr::Name) => quote! {
272                let variants = #inline;
273                let mut variants = variants
274                    .split(',')
275                    .map(|x| x.split_once(" = ").unwrap().1.to_string())
276                    .peekable();
277
278                if variants.peek().is_none() {
279                    return "never".into()
280                }
281
282                let mut buffer = String::new();
283                for variant in variants {
284                    buffer.push_str(&variant);
285                    buffer.push_str(" | ");
286                }
287
288                buffer.trim_end_matches(['|', ' ']).into()
289            },
290            None => quote!(#inline),
291        };
292
293        quote! {
294            fn inline() -> String {
295                #inline
296            }
297
298            fn inline_flattened() -> String {
299                #inline_flattened
300            }
301        }
302    }
303
304    /// Generates the `decl()` and `decl_concrete()` methods.
305    /// `decl_concrete()` is simple, and simply defers to `inline()`.
306    /// For `decl()`, however, we need to change out the generic parameters of the type, replacing
307    /// them with the dummy types generated by `generate_generic_types()`.
308    fn generate_decl_fn(&mut self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
309        let name = &self.ts_name;
310
311        if self.ts_enum.is_some() {
312            let inline = &self.inline;
313            return quote! {
314                fn decl_concrete() -> String {
315                    format!("enum {} {{ {} }}", #name, #inline)
316                }
317
318                fn decl() -> String {
319                    format!("enum {} {{ {} }}", #name, #inline)
320                }
321            };
322        }
323
324        let crate_rename = &self.crate_rename;
325        let generic_types = self.generate_generic_types(generics);
326        let ts_generics = format_generics(
327            &mut self.dependencies,
328            crate_rename,
329            generics,
330            &self.concrete,
331        );
332
333        use GenericParam as G;
334        // These are the generic parameters we'll be using.
335        let generic_idents = generics.params.iter().filter_map(|p| match p {
336            G::Lifetime(_) => None,
337            G::Type(TypeParam { ident, .. }) => match self.concrete.get(ident) {
338                // Since we named our dummy types the same as the generic parameters, we can just keep
339                // the identifier of the generic parameter - its name is shadowed by the dummy struct.
340                None => Some(quote!(#ident)),
341                // If the type parameter is concrete, we use the type the user provided using
342                // `#[ts(concrete)]`
343                Some(concrete) => Some(quote!(#concrete)),
344            },
345            // We keep const parameters as they are, since there's no sensible default value we can
346            // use instead. This might be something to change in the future.
347            G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)),
348        });
349        quote! {
350            fn decl_concrete() -> String {
351                format!("type {} = {};", #name, <Self as #crate_rename::TS>::inline())
352            }
353            fn decl() -> String {
354                #generic_types
355                let inline = <#rust_ty<#(#generic_idents,)*> as #crate_rename::TS>::inline();
356                let generics = #ts_generics;
357                format!("type {}{generics} = {inline};", #name)
358            }
359        }
360    }
361}
362
363fn generate_assoc_type(
364    rust_ty: &Ident,
365    crate_rename: &Path,
366    generics: &Generics,
367    concrete: &HashMap<Ident, Type>,
368) -> TokenStream {
369    use GenericParam as G;
370
371    let generics_params = generics.params.iter().map(|x| match x {
372        G::Type(ty) => match concrete.get(&ty.ident) {
373            None => quote! { #crate_rename::Dummy },
374            Some(ty) => quote! { #ty },
375        },
376        G::Const(ConstParam { ident, .. }) => quote! { #ident },
377        G::Lifetime(LifetimeParam { lifetime, .. }) => quote! { #lifetime },
378    });
379
380    quote! { type WithoutGenerics = #rust_ty<#(#generics_params),*>; }
381}
382
383// generate start of the `impl TS for #ty` block, up to (excluding) the open brace
384fn generate_impl_block_header(
385    crate_rename: &Path,
386    ty: &Ident,
387    generics: &Generics,
388    bounds: Option<&[WherePredicate]>,
389    dependencies: &Dependencies,
390) -> TokenStream {
391    use GenericParam as G;
392
393    let params = generics.params.iter().map(|param| match param {
394        G::Type(TypeParam {
395            ident,
396            colon_token,
397            bounds,
398            ..
399        }) => quote!(#ident #colon_token #bounds),
400        G::Lifetime(LifetimeParam {
401            lifetime,
402            colon_token,
403            bounds,
404            ..
405        }) => quote!(#lifetime #colon_token #bounds),
406        G::Const(ConstParam {
407            const_token,
408            ident,
409            colon_token,
410            ty,
411            ..
412        }) => quote!(#const_token #ident #colon_token #ty),
413    });
414    let type_args = generics.params.iter().map(|param| match param {
415        G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => quote!(#ident),
416        G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
417    });
418
419    let where_bound = match bounds {
420        Some(bounds) => quote! { where #(#bounds),* },
421        None => {
422            let bounds = generate_where_clause(crate_rename, generics, dependencies);
423            quote! { #bounds }
424        }
425    };
426
427    quote!(impl <#(#params),*> #crate_rename::TS for #ty <#(#type_args),*> #where_bound)
428}
429
430fn generate_where_clause(
431    crate_rename: &Path,
432    generics: &Generics,
433    dependencies: &Dependencies,
434) -> WhereClause {
435    let used_types = {
436        let is_type_param = |id: &Ident| generics.type_params().any(|p| &p.ident == id);
437
438        let mut used_types = HashSet::new();
439        for ty in dependencies.used_types() {
440            used_type_params(&mut used_types, ty, is_type_param);
441        }
442        used_types.into_iter()
443    };
444
445    let existing = generics.where_clause.iter().flat_map(|w| &w.predicates);
446    parse_quote! {
447        where #(#existing,)* #(#used_types: #crate_rename::TS),*
448    }
449}
450
451// Extracts all type parameters which are used within the given type.
452// Associated types of a type parameter are extracted as well.
453// Note: This will not extract `I` from `I::Item`, but just `I::Item`!
454fn used_type_params<'ty, 'out>(
455    out: &'out mut HashSet<&'ty Type>,
456    ty: &'ty Type,
457    is_type_param: impl Fn(&'ty Ident) -> bool + Copy + 'out,
458) {
459    use syn::{
460        AngleBracketedGenericArguments as GenericArgs, GenericArgument as G, PathArguments as P,
461    };
462
463    match ty {
464        Type::Array(TypeArray { elem, .. })
465        | Type::Paren(TypeParen { elem, .. })
466        | Type::Reference(TypeReference { elem, .. })
467        | Type::Slice(TypeSlice { elem, .. }) => used_type_params(out, elem, is_type_param),
468        Type::Tuple(TypeTuple { elems, .. }) => elems
469            .iter()
470            .for_each(|elem| used_type_params(out, elem, is_type_param)),
471        Type::Path(TypePath { qself: None, path }) => {
472            let first = path.segments.first().unwrap();
473            if is_type_param(&first.ident) {
474                // The type is either a generic parameter (e.g `T`), or an associated type of that
475                // generic parameter (e.g `I::Item`). Either way, we return it.
476                out.insert(ty);
477                return;
478            }
479
480            let last = path.segments.last().unwrap();
481            if let P::AngleBracketed(GenericArgs { ref args, .. }) = last.arguments {
482                for generic in args {
483                    if let G::Type(ty) = generic {
484                        used_type_params(out, ty, is_type_param);
485                    }
486                }
487            }
488        }
489        _ => (),
490    }
491}
492
493/// Derives [TS](./trait.TS.html) for a struct or enum.
494/// Please take a look at [TS](./trait.TS.html) for documentation.
495#[proc_macro_derive(TS, attributes(ts))]
496pub fn typescript(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
497    match entry(input) {
498        Err(err) => err.to_compile_error(),
499        Ok(result) => result,
500    }
501    .into()
502}
503
504fn entry(input: proc_macro::TokenStream) -> Result<TokenStream> {
505    let input = syn::parse::<Item>(input)?;
506    let (ts, ident, generics) = match input {
507        Item::Struct(s) => (types::struct_def(&s)?, s.ident, s.generics),
508        Item::Enum(e) => (types::enum_def(&e)?, e.ident, e.generics),
509        _ => syn_err!(input.span(); "unsupported item"),
510    };
511
512    Ok(ts.into_impl(ident, generics))
513}