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 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 format!("{dir_or_file}{}.ts", #ts_name)
53 } else {
54 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 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 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 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 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 None => Some(quote!(#ident)),
341 Some(concrete) => Some(quote!(#concrete)),
344 },
345 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
383fn 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
451fn 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 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#[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}