#![macro_use]
#![deny(unused)]
use std::collections::{HashMap, HashSet};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{
parse_quote, spanned::Spanned, ConstParam, Expr, GenericParam, Generics, Item, LifetimeParam,
Path, QSelf, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
TypeTuple, WhereClause, WherePredicate,
};
use crate::{attr::Repr, deps::Dependencies, utils::format_generics};
#[macro_use]
mod utils;
mod attr;
mod deps;
mod optional;
mod types;
struct DerivedTS {
crate_rename: Path,
ts_name: Expr,
docs: Vec<Expr>,
inline: TokenStream,
inline_flattened: Option<TokenStream>,
dependencies: Dependencies,
concrete: HashMap<Ident, Type>,
bound: Option<Vec<WherePredicate>>,
ts_enum: Option<Repr>,
is_enum: TokenStream,
export: bool,
export_to: Option<Expr>,
}
impl DerivedTS {
fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream {
let export = self
.export
.then(|| self.generate_export_test(&rust_ty, &generics));
let output_path_fn = {
let ts_name = &self.ts_name;
let path_string = match &self.export_to {
Some(dir_or_file) => quote![{
let dir_or_file = format!("{}", #dir_or_file);
if dir_or_file.ends_with('/') {
format!("{dir_or_file}{}.ts", #ts_name)
} else {
format!("{dir_or_file}")
}
}],
None => quote![format!("{}.ts", #ts_name)],
};
quote! {
fn output_path() -> Option<std::path::PathBuf> {
Some(std::path::PathBuf::from(#path_string))
}
}
};
let crate_rename = self.crate_rename.clone();
let docs = match &*self.docs {
[] => None,
docs => Some(quote! {
fn docs() -> Option<String> {
Some(#crate_rename::format_docs(&[#(#docs),*]))
}
}),
};
let ident = self.ts_name.clone();
let impl_start = generate_impl_block_header(
&crate_rename,
&rust_ty,
&generics,
self.bound.as_deref(),
&self.dependencies,
);
let assoc_type = generate_assoc_type(&rust_ty, &crate_rename, &generics, &self.concrete);
let name = self.generate_name_fn(&generics);
let is_enum = self.is_enum.clone();
let inline = self.generate_inline_fn();
let decl = self.generate_decl_fn(&rust_ty, &generics);
let dependencies = &self.dependencies;
let generics_fn = self.generate_generics_fn(&generics);
quote! {
#[automatically_derived]
#impl_start {
#assoc_type
type OptionInnerType = Self;
const IS_ENUM: bool = #is_enum;
fn ident(cfg: &#crate_rename::Config) -> String {
(#ident).to_string()
}
#docs
#name
#decl
#inline
#generics_fn
#output_path_fn
fn visit_dependencies(v: &mut impl #crate_rename::TypeVisitor)
where
Self: 'static,
{
#dependencies
}
}
#export
}
}
fn name_with_generics(&self, generics: &Generics) -> TokenStream {
let name = &self.ts_name;
let crate_rename = &self.crate_rename;
let mut generics_ts_names = generics
.type_params()
.filter(|ty| !self.concrete.contains_key(&ty.ident))
.map(|ty| &ty.ident)
.map(|generic| quote!(<#generic as #crate_rename::TS>::name(cfg)))
.peekable();
if generics_ts_names.peek().is_some() {
quote! {
format!("{}<{}>", #name, vec![#(#generics_ts_names),*].join(", "))
}
} else {
quote!((#name).to_string())
}
}
fn generate_generic_types(&self, generics: &Generics) -> TokenStream {
let crate_rename = &self.crate_rename;
let generics = generics
.type_params()
.filter(|ty| !self.concrete.contains_key(&ty.ident))
.map(|ty| ty.ident.clone());
let name = quote![<Self as #crate_rename::TS>::name(cfg)];
quote! {
#(
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct #generics;
impl std::fmt::Display for #generics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl #crate_rename::TS for #generics {
type WithoutGenerics = #generics;
type OptionInnerType = Self;
fn name(cfg: &#crate_rename::Config) -> String { stringify!(#generics).to_owned() }
fn inline(cfg: &#crate_rename::Config) -> String { panic!("{} cannot be inlined", #name) }
fn inline_flattened(cfg: &#crate_rename::Config) -> String { stringify!(#generics).to_owned() }
fn decl(cfg: &#crate_rename::Config) -> String { panic!("{} cannot be declared", #name) }
fn decl_concrete(cfg: &#crate_rename::Config) -> String { panic!("{} cannot be declared", #name) }
}
)*
}
}
fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
let test_fn = format_ident!(
"export_bindings_{}",
rust_ty.to_string().to_lowercase().replace("r#", "")
);
let crate_rename = &self.crate_rename;
let generic_params = generics
.type_params()
.map(|ty| match self.concrete.get(&ty.ident) {
None => quote! { #crate_rename::Dummy },
Some(ty) => quote! { #ty },
});
let ty = quote!(<#rust_ty<#(#generic_params),*> as #crate_rename::TS>);
quote! {
#[cfg(test)]
#[test]
fn #test_fn() {
let cfg = #crate_rename::Config::from_env();
#ty::export_all(&cfg).expect("could not export type");
}
}
}
fn generate_generics_fn(&self, generics: &Generics) -> TokenStream {
let crate_rename = &self.crate_rename;
let generics = generics
.type_params()
.filter(|ty| !self.concrete.contains_key(&ty.ident))
.map(|TypeParam { ident, .. }| {
quote![
v.visit::<#ident>();
<#ident as #crate_rename::TS>::visit_generics(v);
]
});
quote! {
fn visit_generics(v: &mut impl #crate_rename::TypeVisitor)
where
Self: 'static,
{
#(#generics)*
}
}
}
fn generate_name_fn(&self, generics: &Generics) -> TokenStream {
let crate_rename = &self.crate_rename;
let name = self.name_with_generics(generics);
quote! {
fn name(cfg: &#crate_rename::Config) -> String {
#name
}
}
}
fn generate_inline_fn(&self) -> TokenStream {
let inline = &self.inline;
let crate_rename = &self.crate_rename;
let inline_flattened = self.inline_flattened.clone().unwrap_or_else(|| {
quote! {
panic!("{} cannot be flattened", <Self as #crate_rename::TS>::name(cfg))
}
});
let inline = match self.ts_enum {
Some(Repr::Int) => quote! {
let variants = #inline.replace(|x: char| !x.is_numeric() && x != ',', "");
let mut variants = variants
.split(',')
.map(|x| x.parse().ok())
.peekable();
if variants.peek().is_none() {
return "never".into()
}
let mut buffer = String::new();
let mut latest = None::<isize>;
for variant in variants {
let value = variant.or(latest.map(|x| x + 1)).unwrap_or(0);
buffer.push_str(&format!("{} | ", value));
latest = Some(value)
}
buffer.trim_end_matches(['|', ' ']).into()
},
Some(Repr::Name) => quote! {
let variants = #inline;
let mut variants = variants
.split(',')
.map(|x| x.split_once(" = ").unwrap().1.to_string())
.peekable();
if variants.peek().is_none() {
return "never".into()
}
let mut buffer = String::new();
for variant in variants {
buffer.push_str(&variant);
buffer.push_str(" | ");
}
buffer.trim_end_matches(['|', ' ']).into()
},
None => quote!(#inline),
};
quote! {
fn inline(cfg: &#crate_rename::Config) -> String {
#inline
}
fn inline_flattened(cfg: &#crate_rename::Config) -> String {
#inline_flattened
}
}
}
fn generate_decl_fn(&mut self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
let crate_rename = &self.crate_rename;
let name = &self.ts_name;
if self.ts_enum.is_some() {
let inline = &self.inline;
return quote! {
fn decl_concrete(cfg: &#crate_rename::Config) -> String {
format!("enum {} {{ {} }}", #name, #inline)
}
fn decl(cfg: &#crate_rename::Config) -> String {
format!("enum {} {{ {} }}", #name, #inline)
}
};
}
let generic_types = self.generate_generic_types(generics);
let ts_generics = format_generics(
&mut self.dependencies,
crate_rename,
generics,
&self.concrete,
);
use GenericParam as G;
let generic_idents = generics.params.iter().filter_map(|p| match p {
G::Lifetime(_) => None,
G::Type(TypeParam { ident, .. }) => match self.concrete.get(ident) {
None => Some(quote!(#ident)),
Some(concrete) => Some(quote!(#concrete)),
},
G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)),
});
quote! {
fn decl_concrete(cfg: &#crate_rename::Config) -> String {
format!("type {} = {};", #name, <Self as #crate_rename::TS>::inline(cfg))
}
fn decl(cfg: &#crate_rename::Config) -> String {
#generic_types
let inline = <#rust_ty<#(#generic_idents,)*> as #crate_rename::TS>::inline(cfg);
let generics = #ts_generics;
format!("type {}{generics} = {inline};", #name)
}
}
}
}
fn generate_assoc_type(
rust_ty: &Ident,
crate_rename: &Path,
generics: &Generics,
concrete: &HashMap<Ident, Type>,
) -> TokenStream {
use GenericParam as G;
let generics_params = generics.params.iter().map(|x| match x {
G::Type(ty) => match concrete.get(&ty.ident) {
None => quote! { #crate_rename::Dummy },
Some(ty) => quote! { #ty },
},
G::Const(ConstParam { ident, .. }) => quote! { #ident },
G::Lifetime(LifetimeParam { lifetime, .. }) => quote! { #lifetime },
});
quote! { type WithoutGenerics = #rust_ty<#(#generics_params),*>; }
}
fn generate_impl_block_header(
crate_rename: &Path,
ty: &Ident,
generics: &Generics,
bounds: Option<&[WherePredicate]>,
dependencies: &Dependencies,
) -> TokenStream {
use GenericParam as G;
let params = generics.params.iter().map(|param| match param {
G::Type(TypeParam {
ident,
colon_token,
bounds,
..
}) => quote!(#ident #colon_token #bounds),
G::Lifetime(LifetimeParam {
lifetime,
colon_token,
bounds,
..
}) => quote!(#lifetime #colon_token #bounds),
G::Const(ConstParam {
const_token,
ident,
colon_token,
ty,
..
}) => quote!(#const_token #ident #colon_token #ty),
});
let type_args = generics.params.iter().map(|param| match param {
G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => quote!(#ident),
G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
});
let where_bound = match bounds {
Some(bounds) => quote! { where #(#bounds),* },
None => {
let bounds = generate_where_clause(crate_rename, generics, dependencies);
quote! { #bounds }
}
};
quote!(impl <#(#params),*> #crate_rename::TS for #ty <#(#type_args),*> #where_bound)
}
fn generate_where_clause(
crate_rename: &Path,
generics: &Generics,
dependencies: &Dependencies,
) -> WhereClause {
let used_types = {
let is_type_param = |id: &Ident| generics.type_params().any(|p| &p.ident == id);
let mut used_types = HashSet::new();
for ty in dependencies.used_types() {
used_type_params(&mut used_types, ty, is_type_param);
}
used_types.into_iter()
};
let existing = generics.where_clause.iter().flat_map(|w| &w.predicates);
parse_quote! {
where #(#existing,)* #(#used_types: #crate_rename::TS),*
}
}
fn used_type_params<'ty, 'out>(
out: &'out mut HashSet<&'ty Type>,
ty: &'ty Type,
is_type_param: impl Fn(&'ty Ident) -> bool + Copy + 'out,
) {
use syn::{
AngleBracketedGenericArguments as GenericArgs, GenericArgument as G, PathArguments as P,
};
match ty {
Type::Array(TypeArray { elem, .. })
| Type::Paren(TypeParen { elem, .. })
| Type::Reference(TypeReference { elem, .. })
| Type::Slice(TypeSlice { elem, .. }) => used_type_params(out, elem, is_type_param),
Type::Tuple(TypeTuple { elems, .. }) => elems
.iter()
.for_each(|elem| used_type_params(out, elem, is_type_param)),
Type::Path(TypePath { qself: None, path }) => {
let first = path.segments.first().unwrap();
if is_type_param(&first.ident) {
out.insert(ty);
return;
}
let last = path.segments.last().unwrap();
if let P::AngleBracketed(GenericArgs { ref args, .. }) = last.arguments {
for generic in args {
if let G::Type(ty) = generic {
used_type_params(out, ty, is_type_param);
}
}
}
}
Type::Path(TypePath {
qself: Some(QSelf { ty, .. }),
..
}) => {
used_type_params(out, ty, is_type_param);
}
_ => (),
}
}
#[proc_macro_derive(TS, attributes(ts))]
pub fn typescript(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
match entry(input) {
Err(err) => err.to_compile_error(),
Ok(result) => result,
}
.into()
}
fn entry(input: proc_macro::TokenStream) -> Result<TokenStream> {
let input = syn::parse::<Item>(input)?;
let (ts, ident, generics) = match input {
Item::Struct(s) => (types::struct_def(&s)?, s.ident, s.generics),
Item::Enum(e) => (types::enum_def(&e)?, e.ident, e.generics),
_ => syn_err!(input.span(); "unsupported item"),
};
Ok(ts.into_impl(ident, generics))
}