diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 43da5a7..9349faf 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -75,3 +75,8 @@ jobs: with: command: fmt args: --all -- --check + + - name: Cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy diff --git a/tsify-next-macros/src/attrs.rs b/tsify-next-macros/src/attrs.rs index c67139f..4ef01c3 100644 --- a/tsify-next-macros/src/attrs.rs +++ b/tsify-next-macros/src/attrs.rs @@ -2,32 +2,43 @@ use serde_derive_internals::ast::Field; use crate::comments::extract_doc_comments; +/// Attributes that can be applied to a type decorated with `#[derive(Tsify)]`. +/// E.g., through `#[tsify(into_wasm_abi)]`. #[derive(Debug, Default)] pub struct TsifyContainerAttrs { + /// Implement `IntoWasmAbi` for the type. pub into_wasm_abi: bool, + /// Implement `FromWasmAbi` for the type. pub from_wasm_abi: bool, + /// Whether the type should be wrapped in a Typescript namespace. pub namespace: bool, + /// Information about how the type should be serialized. pub ty_config: TypeGenerationConfig, + /// Comments associated with the type. These will be written out to the generated Typescript. pub comments: Vec, } +/// Configuration affecting how Typescript types are generated. #[derive(Debug, Default)] pub struct TypeGenerationConfig { + /// Universal prefix for generated types pub type_prefix: Option, + /// Universal suffix for generated types pub type_suffix: Option, + /// Whether missing fields should be represented as null in Typescript pub missing_as_null: bool, + /// Whether a hashmap should be represented as an object in Typescript pub hashmap_as_object: bool, + /// Whether large number types should be represented as BigInts in Typescript pub large_number_types_as_bigints: bool, } + impl TypeGenerationConfig { - pub fn format_name(&self, mut name: String) -> String { - if let Some(ref prefix) = self.type_prefix { - name.insert_str(0, prefix); - } - if let Some(ref suffix) = self.type_suffix { - name.push_str(suffix); - } - name + /// Format a type `name` adding a prefix and suffix if they are set. + pub fn format_name(&self, name: String) -> String { + let prefix = self.type_prefix.as_ref().map_or("", String::as_str); + let suffix = self.type_suffix.as_ref().map_or("", String::as_str); + format!("{}{}{}", prefix, name, suffix) } } @@ -131,7 +142,7 @@ impl TsifyContainerAttrs { return Ok(()); } - Err(meta.error("unsupported tsify attribute, expected one of `into_wasm_abi`, `from_wasm_abi`, `namespace`, 'type_prefix', 'type_suffix', 'missing_as_null', 'hashmap_as_object', 'large_number_types_as_bigints'")) + Err(meta.error("unsupported tsify attribute, expected one of `into_wasm_abi`, `from_wasm_abi`, `namespace`, `type_prefix`, `type_suffix`, `missing_as_null`, `hashmap_as_object`, `large_number_types_as_bigints`")) })?; } diff --git a/tsify-next-macros/src/comments.rs b/tsify-next-macros/src/comments.rs index 1364bd6..9f52792 100644 --- a/tsify-next-macros/src/comments.rs +++ b/tsify-next-macros/src/comments.rs @@ -10,11 +10,7 @@ pub fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { .filter_map(|a| { // if the path segments include an ident of "doc" we know this // this is a doc comment - if a.path() - .segments - .iter() - .any(|s| s.ident.to_string() == "doc") - { + if a.path().segments.iter().any(|s| s.ident == "doc") { Some(a.to_token_stream().into_iter().filter_map(|t| match t { TokenTree::Group(group) => { // this will return the inner tokens of the group @@ -48,6 +44,7 @@ pub fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { }) } +/// Output extracted doc comments as Typescript doc comments. pub fn write_doc_comments( f: &mut std::fmt::Formatter<'_>, comments: &Vec, @@ -62,10 +59,11 @@ pub fn write_doc_comments( .collect::>() .join(""); - write!(f, "{}", format!("/**\n{} */\n", comment)) + write!(f, "{}", format_args!("/**\n{} */\n", comment)) } -pub fn clean_comments(typ: &mut TsType) -> () { +/// Remove all comments from a `TsType::TypeLit` +pub fn clean_comments(typ: &mut TsType) { if let TsType::TypeLit(ref mut lit) = typ { lit.members.iter_mut().for_each(|elem| { elem.comments = vec![]; diff --git a/tsify-next-macros/src/container.rs b/tsify-next-macros/src/container.rs index 1058892..d7af5f1 100644 --- a/tsify-next-macros/src/container.rs +++ b/tsify-next-macros/src/container.rs @@ -1,12 +1,19 @@ use serde_derive_internals::{ast, ast::Container as SerdeContainer, attr}; -use crate::{attrs::TsifyContainerAttrs, ctxt::Ctxt}; +use crate::{attrs::TsifyContainerAttrs, error_tracker::ErrorTracker}; +/// Data structure storing information about a type decorated with `#[derive(Tsify)]`. +/// This structure also keeps information that was parsed via Serde's macros. pub struct Container<'a> { - pub ctxt: Ctxt, + /// Errors that occurred during processing. + pub errors: ErrorTracker, + /// Attributes passed to the `#[derive(Tsify)]` macro. pub attrs: TsifyContainerAttrs, + /// Information about the type as parsed by Serde. pub serde_container: SerdeContainer<'a>, + /// The `ident` of the type as written in the Rust code. pub ident_str: String, + /// The name type that will be serialized to Typescript. pub name: String, } @@ -14,12 +21,12 @@ impl<'a> Container<'a> { pub fn new(serde_container: SerdeContainer<'a>) -> Self { let input = &serde_container.original; let attrs = TsifyContainerAttrs::from_derive_input(input); - let ctxt = Ctxt::new(); + let errors = ErrorTracker::new(); let attrs = match attrs { Ok(attrs) => attrs, Err(err) => { - ctxt.syn_error(err); + errors.syn_error(err); Default::default() } }; @@ -33,7 +40,7 @@ impl<'a> Container<'a> { .format_name(serde_container.ident.to_string()); Self { - ctxt, + errors, attrs, serde_container, ident_str, @@ -55,10 +62,12 @@ impl<'a> Container<'a> { } } + /// The `ident` of the type as written in the Rust code. pub fn ident(&self) -> &syn::Ident { &self.serde_container.ident } + /// The `ident` of the type as written in the Rust code as a string. pub fn ident_str(&self) -> String { self.ident_str.clone() } @@ -68,27 +77,33 @@ impl<'a> Container<'a> { &self.serde_container.attrs } + /// Whether or not Serde has marked this type as `transparent`. pub fn transparent(&self) -> bool { self.serde_attrs().transparent() } + /// The name of the type that will be serialized to Typescript. pub fn name(&self) -> String { self.name.clone() } + /// Information about the generics associated with the type as parsed by Serde. pub fn generics(&self) -> &syn::Generics { self.serde_container.generics } + /// Information about the data fields of the type as parsed by Serde. pub fn serde_data(&self) -> &ast::Data { &self.serde_container.data } + /// Add a new error to the list of processing errors. pub fn syn_error(&self, err: syn::Error) { - self.ctxt.syn_error(err); + self.errors.syn_error(err); } + /// Return all accumulated errors. pub fn check(self) -> syn::Result<()> { - self.ctxt.check() + self.errors.check() } } diff --git a/tsify-next-macros/src/decl.rs b/tsify-next-macros/src/decl.rs index 5c36c57..de0c69d 100644 --- a/tsify-next-macros/src/decl.rs +++ b/tsify-next-macros/src/decl.rs @@ -20,7 +20,7 @@ impl TsTypeAliasDecl { pub fn to_string_with_indent(&self, indent: usize) -> String { let out = self.to_string(); let indent_str = " ".repeat(indent); - out.split("\n") + out.split('\n') .map(|line| format!("{}{}", indent_str, line)) .collect::>() .join("\n") diff --git a/tsify-next-macros/src/ctxt.rs b/tsify-next-macros/src/error_tracker.rs similarity index 53% rename from tsify-next-macros/src/ctxt.rs rename to tsify-next-macros/src/error_tracker.rs index 573e848..13da153 100644 --- a/tsify-next-macros/src/ctxt.rs +++ b/tsify-next-macros/src/error_tracker.rs @@ -1,20 +1,35 @@ use std::cell::RefCell; -pub struct Ctxt { +/// Tracks errors during macro expansion. This struct implements a panic on `Drop` +/// if there are accumulated errors that weren't checked. +/// +/// By using an error tracker, you can accumulate errors inside of closures and still propagate +/// them when needed. +/// +/// # Example +/// ```ignore +/// let errors = ErrorTracker::new(); +/// errors.syn_error(syn::Error::new_spanned(ident, "error message")); +/// // Make sure to check the errors or else you'll get a panic. +/// errors.check()?; +/// ``` +pub struct ErrorTracker { errors: RefCell>>, } -impl Ctxt { +impl ErrorTracker { pub fn new() -> Self { Self { errors: RefCell::new(Some(Vec::new())), } } + /// Add a `syn::Error` to the list of errors. pub fn syn_error(&self, err: syn::Error) { self.errors.borrow_mut().as_mut().unwrap().push(err) } + /// Return all accumulated errors. This also clears the list of errors. pub fn check(self) -> syn::Result<()> { let mut errors = self.errors.take().unwrap().into_iter(); @@ -31,7 +46,7 @@ impl Ctxt { } } -impl Drop for Ctxt { +impl Drop for ErrorTracker { fn drop(&mut self) { if !std::thread::panicking() && self.errors.borrow().is_some() { panic!("forgot to check for errors"); diff --git a/tsify-next-macros/src/lib.rs b/tsify-next-macros/src/lib.rs index 0e261e6..7005a31 100644 --- a/tsify-next-macros/src/lib.rs +++ b/tsify-next-macros/src/lib.rs @@ -1,9 +1,9 @@ mod attrs; mod comments; mod container; -mod ctxt; mod decl; mod derive; +mod error_tracker; mod parser; mod type_alias; mod typescript; @@ -16,7 +16,7 @@ fn declare_impl( item: syn::Item, ) -> syn::Result { match item { - syn::Item::Type(item) => type_alias::expend(item), + syn::Item::Type(item) => type_alias::expand(item), syn::Item::Enum(item) => derive::expand_by_attr(args, item.into()), syn::Item::Struct(item) => derive::expand_by_attr(args, item.into()), _ => Err(syn::Error::new_spanned( @@ -26,6 +26,7 @@ fn declare_impl( } } +/// The `declare` macro, used in `#[declare]` annotations. #[proc_macro_attribute] pub fn declare( args: proc_macro::TokenStream, @@ -39,6 +40,7 @@ pub fn declare( .into() } +/// The `Tsify` derive macro, used in `#[derive(Tsify, ...)]` annotations. #[proc_macro_derive(Tsify, attributes(tsify, serde))] pub fn derive_tsify(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let item: DeriveInput = parse_macro_input!(input); diff --git a/tsify-next-macros/src/parser.rs b/tsify-next-macros/src/parser.rs index 4bf44db..67c5933 100644 --- a/tsify-next-macros/src/parser.rs +++ b/tsify-next-macros/src/parser.rs @@ -63,7 +63,6 @@ impl<'a> Parser<'a> { self.container .generics() .type_params() - .into_iter() .map(|p| p.ident.to_string()) .filter(|t| type_ref_names.contains(t)) .collect() diff --git a/tsify-next-macros/src/type_alias.rs b/tsify-next-macros/src/type_alias.rs index 0b0527d..a61cef9 100644 --- a/tsify-next-macros/src/type_alias.rs +++ b/tsify-next-macros/src/type_alias.rs @@ -2,12 +2,13 @@ use proc_macro2::TokenStream; use quote::quote; use crate::{ - attrs::TypeGenerationConfig, comments::extract_doc_comments, ctxt::Ctxt, decl::TsTypeAliasDecl, - typescript::TsType, + attrs::TypeGenerationConfig, comments::extract_doc_comments, decl::TsTypeAliasDecl, + error_tracker::ErrorTracker, typescript::TsType, }; -pub fn expend(item: syn::ItemType) -> syn::Result { - let ctxt = Ctxt::new(); +/// Expand a `#[declare]` macro on a Rust `type = ...` expression. +pub fn expand(item: syn::ItemType) -> syn::Result { + let errors = ErrorTracker::new(); let type_ann = TsType::from_syn_type(&TypeGenerationConfig::default(), item.ty.as_ref()); @@ -34,7 +35,7 @@ pub fn expend(item: syn::ItemType) -> syn::Result { }; }; - ctxt.check()?; + errors.check()?; let tokens = quote! { #item diff --git a/tsify-next-macros/src/typescript.rs b/tsify-next-macros/src/typescript.rs deleted file mode 100644 index 4194ae0..0000000 --- a/tsify-next-macros/src/typescript.rs +++ /dev/null @@ -1,891 +0,0 @@ -use std::{collections::HashSet, fmt::Display}; - -use serde_derive_internals::{ast::Style, attr::TagType}; - -use crate::{attrs::TypeGenerationConfig, comments::write_doc_comments}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TsKeywordTypeKind { - Number, - Bigint, - Boolean, - String, - Void, - Undefined, - Null, - Never, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TsTypeElement { - pub key: String, - pub type_ann: TsType, - pub optional: bool, - pub comments: Vec, -} - -impl From for TsTypeLit { - fn from(m: TsTypeElement) -> Self { - TsTypeLit { members: vec![m] } - } -} - -impl From for TsType { - fn from(m: TsTypeElement) -> Self { - TsType::TypeLit(m.into()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TsTypeLit { - pub members: Vec, -} - -impl From for TsType { - fn from(lit: TsTypeLit) -> Self { - TsType::TypeLit(lit) - } -} - -impl TsTypeLit { - fn get_mut(&mut self, key: &String) -> Option<&mut TsTypeElement> { - self.members.iter_mut().find(|member| &member.key == key) - } - - fn and(self, other: Self) -> Self { - let init = TsTypeLit { members: vec![] }; - - self.members - .into_iter() - .chain(other.members.into_iter()) - .fold(init, |mut acc, m| { - if let Some(acc_m) = acc.get_mut(&m.key) { - let mut tmp = TsType::NULL; - std::mem::swap(&mut acc_m.type_ann, &mut tmp); - acc_m.type_ann = tmp.and(m.type_ann); - } else { - acc.members.push(m) - } - - acc - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum NullType { - Null, - Undefined, -} - -impl NullType { - pub const fn new(config: &TypeGenerationConfig) -> Self { - if cfg!(feature = "js") && !config.missing_as_null { - Self::Undefined - } else { - Self::Null - } - } - - pub const fn to_type(&self) -> TsType { - match self { - Self::Null => TsType::NULL, - Self::Undefined => TsType::UNDEFINED, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TsType { - Keyword(TsKeywordTypeKind), - Lit(String), - Array(Box), - Tuple(Vec), - Option(Box, NullType), - Ref { - name: String, - type_params: Vec, - }, - Fn { - params: Vec, - type_ann: Box, - }, - TypeLit(TsTypeLit), - Intersection(Vec), - Union(Vec), - Override { - type_override: String, - type_params: Vec, - }, -} - -macro_rules! type_lit { - ($($k: ident: $t: path);* $(;)?) => { - TsType::TypeLit(TsTypeLit { - members: vec![$( - TsTypeElement { - key: stringify!($k).to_string(), - type_ann: $t, - optional: false, - comments: vec![], - } - ),*], - }) - }; -} - -impl From for TsType { - fn from(kind: TsKeywordTypeKind) -> Self { - Self::Keyword(kind) - } -} - -impl TsType { - pub const NUMBER: TsType = TsType::Keyword(TsKeywordTypeKind::Number); - pub const BIGINT: TsType = TsType::Keyword(TsKeywordTypeKind::Bigint); - pub const BOOLEAN: TsType = TsType::Keyword(TsKeywordTypeKind::Boolean); - pub const STRING: TsType = TsType::Keyword(TsKeywordTypeKind::String); - pub const VOID: TsType = TsType::Keyword(TsKeywordTypeKind::Void); - pub const UNDEFINED: TsType = TsType::Keyword(TsKeywordTypeKind::Undefined); - pub const NULL: TsType = TsType::Keyword(TsKeywordTypeKind::Null); - pub const NEVER: TsType = TsType::Keyword(TsKeywordTypeKind::Never); - - pub const fn nullish(config: &TypeGenerationConfig) -> Self { - NullType::new(config).to_type() - } - - pub const fn empty_type_lit() -> Self { - Self::TypeLit(TsTypeLit { members: vec![] }) - } - - pub fn is_ref(&self) -> bool { - matches!(self, Self::Ref { .. }) - } - - pub fn and(self, other: Self) -> Self { - match (self, other) { - (TsType::TypeLit(x), TsType::TypeLit(y)) => x.and(y).into(), - (TsType::Intersection(x), TsType::Intersection(y)) => { - let mut vec = Vec::with_capacity(x.len() + y.len()); - vec.extend(x); - vec.extend(y); - TsType::Intersection(vec) - } - (TsType::Intersection(x), y) => { - let mut vec = Vec::with_capacity(x.len() + 1); - vec.extend(x); - vec.push(y); - TsType::Intersection(vec) - } - (x, TsType::Intersection(y)) => { - let mut vec = Vec::with_capacity(y.len() + 1); - vec.push(x); - vec.extend(y); - TsType::Intersection(vec) - } - (x, y) => TsType::Intersection(vec![x, y]), - } - } - - pub fn from_syn_type(config: &TypeGenerationConfig, ty: &syn::Type) -> Self { - use syn::Type::*; - use syn::{ - TypeArray, TypeBareFn, TypeGroup, TypeImplTrait, TypeParamBound, TypeParen, TypePath, - TypeReference, TypeSlice, TypeTraitObject, TypeTuple, - }; - - match ty { - Array(TypeArray { elem, len, .. }) => { - let elem = Self::from_syn_type(config, elem); - let len = parse_len(len); - - match len { - Some(len) if len <= 16 => Self::Tuple(vec![elem; len]), - _ => Self::Array(Box::new(elem)), - } - } - - Slice(TypeSlice { elem, .. }) => { - Self::Array(Box::new(Self::from_syn_type(config, elem))) - } - - Reference(TypeReference { elem, .. }) - | Paren(TypeParen { elem, .. }) - | Group(TypeGroup { elem, .. }) => Self::from_syn_type(config, elem), - - BareFn(TypeBareFn { inputs, output, .. }) => { - let params = inputs - .iter() - .map(|arg| Self::from_syn_type(config, &arg.ty)) - .collect(); - - let type_ann = if let syn::ReturnType::Type(_, ty) = output { - Self::from_syn_type(config, ty) - } else { - TsType::VOID - }; - - Self::Fn { - params, - type_ann: Box::new(type_ann), - } - } - - Tuple(TypeTuple { elems, .. }) => { - if elems.is_empty() { - TsType::nullish(config) - } else { - let elems = elems - .iter() - .map(|ty| Self::from_syn_type(config, ty)) - .collect(); - Self::Tuple(elems) - } - } - - Path(TypePath { path, .. }) => Self::from_path(config, path).unwrap_or(TsType::NEVER), - - TraitObject(TypeTraitObject { bounds, .. }) - | ImplTrait(TypeImplTrait { bounds, .. }) => { - let elems = bounds - .iter() - .filter_map(|t| match t { - TypeParamBound::Trait(t) => Self::from_path(config, &t.path), - _ => None, // skip lifetime etc. - }) - .collect(); - - Self::Intersection(elems) - } - - Ptr(_) | Infer(_) | Macro(_) | Never(_) | Verbatim(_) => TsType::NEVER, - - _ => TsType::NEVER, - } - } - - fn from_path(config: &TypeGenerationConfig, path: &syn::Path) -> Option { - path.segments - .last() - .map(|segment| Self::from_path_segment(config, segment)) - } - - fn from_path_segment(config: &TypeGenerationConfig, segment: &syn::PathSegment) -> Self { - let name = segment.ident.to_string(); - - let (args, output) = match &segment.arguments { - syn::PathArguments::AngleBracketed(path) => { - let args = path - .args - .iter() - .filter_map(|p| match p { - syn::GenericArgument::Type(t) => Some(t), - syn::GenericArgument::AssocType(t) => Some(&t.ty), - _ => None, - }) - .collect(); - - (args, None) - } - - syn::PathArguments::Parenthesized(path) => { - let args = path.inputs.iter().collect(); - - let output = match &path.output { - syn::ReturnType::Default => None, - syn::ReturnType::Type(_, tp) => Some(tp.as_ref()), - }; - - (args, output) - } - - syn::PathArguments::None => (vec![], None), - }; - - match name.as_str() { - "u8" | "u16" | "u32" | "i8" | "i16" | "i32" | "f64" | "f32" => Self::NUMBER, - - "usize" | "isize" | "u64" | "i64" => { - if cfg!(feature = "js") && config.large_number_types_as_bigints { - Self::BIGINT - } else { - Self::NUMBER - } - } - - "u128" | "i128" => { - if cfg!(feature = "js") { - Self::BIGINT - } else { - Self::NUMBER - } - } - - "String" | "str" | "char" | "Path" | "PathBuf" => Self::STRING, - - "bool" => Self::BOOLEAN, - - "Box" | "Cow" | "Rc" | "Arc" | "Cell" | "RefCell" if args.len() == 1 => { - Self::from_syn_type(config, args[0]) - } - - "Vec" | "VecDeque" | "LinkedList" if args.len() == 1 => { - let elem = Self::from_syn_type(config, args[0]); - Self::Array(Box::new(elem)) - } - - "HashMap" | "BTreeMap" if args.len() == 2 => { - let type_params = args - .iter() - .map(|arg| Self::from_syn_type(config, arg)) - .collect(); - - let name = if cfg!(feature = "js") && !config.hashmap_as_object { - "Map" - } else { - "Record" - } - .to_string(); - - Self::Ref { name, type_params } - } - - "HashSet" | "BTreeSet" if args.len() == 1 => { - let elem = Self::from_syn_type(config, args[0]); - Self::Array(Box::new(elem)) - } - - "Option" if args.len() == 1 => Self::Option( - Box::new(Self::from_syn_type(config, args[0])), - NullType::new(config), - ), - - "ByteBuf" => { - if cfg!(feature = "js") { - Self::Ref { - name: String::from("Uint8Array"), - type_params: vec![], - } - } else { - Self::Array(Box::new(Self::NUMBER)) - } - } - - "Result" if args.len() == 2 => { - let arg0 = Self::from_syn_type(config, args[0]); - let arg1 = Self::from_syn_type(config, args[1]); - - let ok = type_lit! { Ok: arg0 }; - let err = type_lit! { Err: arg1 }; - - Self::Union(vec![ok, err]) - } - - "Duration" => type_lit! { - secs: Self::NUMBER; - nanos: Self::NUMBER; - }, - - "SystemTime" => type_lit! { - secs_since_epoch: Self::NUMBER; - nanos_since_epoch: Self::NUMBER; - }, - - "Range" | "RangeInclusive" => { - let start = Self::from_syn_type(config, args[0]); - let end = start.clone(); - - type_lit! { - start: start; - end: end; - } - } - - "Fn" | "FnOnce" | "FnMut" => { - let params = args - .into_iter() - .map(|ty| Self::from_syn_type(config, ty)) - .collect(); - let type_ann = output - .map(|ty| Self::from_syn_type(config, ty)) - .unwrap_or_else(|| TsType::VOID); - - Self::Fn { - params, - type_ann: Box::new(type_ann), - } - } - _ => { - let type_params = args - .into_iter() - .map(|ty| Self::from_syn_type(config, ty)) - .collect(); - Self::Ref { - name: config.format_name(name), - type_params, - } - } - } - } - - pub fn with_tag_type( - self, - config: &TypeGenerationConfig, - name: String, - style: Style, - tag_type: &TagType, - ) -> Self { - let type_ann = self; - - match tag_type { - TagType::External => { - if matches!(style, Style::Unit) { - TsType::Lit(name) - } else { - TsTypeElement { - key: name, - type_ann, - optional: false, - comments: vec![], - } - .into() - } - } - TagType::Internal { tag } => { - if type_ann == TsType::nullish(config) { - let tag_field: TsType = TsTypeElement { - key: tag.clone(), - type_ann: TsType::Lit(name), - optional: false, - comments: vec![], - } - .into(); - - tag_field - } else { - let tag_field: TsType = TsTypeElement { - key: tag.clone(), - type_ann: TsType::Lit(name), - optional: false, - comments: vec![], - } - .into(); - - tag_field.and(type_ann) - } - } - TagType::Adjacent { tag, content } => { - let tag_field = TsTypeElement { - key: tag.clone(), - type_ann: TsType::Lit(name), - optional: false, - comments: vec![], - }; - - if matches!(style, Style::Unit) { - tag_field.into() - } else { - let content_field = TsTypeElement { - key: content.clone(), - type_ann, - optional: false, - comments: vec![], - }; - - TsTypeLit { - members: vec![tag_field, content_field], - } - .into() - } - } - TagType::None => type_ann, - } - } - - pub fn visit<'a, F: FnMut(&'a TsType)>(&'a self, f: &mut F) { - f(self); - - match self { - TsType::Ref { type_params, .. } => { - type_params.iter().for_each(|t| t.visit(f)); - } - TsType::Array(elem) => elem.visit(f), - TsType::Tuple(elems) => { - elems.iter().for_each(|t| t.visit(f)); - } - TsType::Option(t, _) => t.visit(f), - TsType::Fn { params, type_ann } => { - params - .iter() - .chain(Some(type_ann.as_ref())) - .for_each(|t| t.visit(f)); - } - TsType::TypeLit(TsTypeLit { members }) => { - members.iter().for_each(|m| m.type_ann.visit(f)); - } - TsType::Intersection(tys) | TsType::Union(tys) => { - tys.iter().for_each(|t| t.visit(f)); - } - TsType::Keyword(_) | TsType::Lit(_) | TsType::Override { .. } => (), - } - } - - pub fn type_ref_names(&self) -> HashSet<&String> { - let mut set: HashSet<&String> = HashSet::new(); - - self.visit(&mut |ty: &TsType| match ty { - TsType::Ref { name, .. } => { - set.insert(name); - } - TsType::Override { type_params, .. } => set.extend(type_params), - _ => (), - }); - - set - } - - pub fn prefix_type_refs(self, prefix: &String, exceptions: &Vec) -> Self { - match self { - TsType::Array(t) => TsType::Array(Box::new(t.prefix_type_refs(prefix, exceptions))), - TsType::Tuple(tv) => TsType::Tuple( - tv.iter() - .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) - .collect(), - ), - TsType::Option(t, null) => { - TsType::Option(Box::new(t.prefix_type_refs(prefix, exceptions)), null) - } - TsType::Ref { name, type_params } => { - if exceptions.contains(&name) { - TsType::Ref { - name, - type_params: type_params - .iter() - .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) - .collect(), - } - } else { - TsType::Ref { - name: format!("{}{}", prefix, name), - type_params: type_params - .iter() - .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) - .collect(), - } - } - } - TsType::Fn { params, type_ann } => TsType::Fn { - params: params - .iter() - .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) - .collect(), - type_ann: Box::new(type_ann.prefix_type_refs(prefix, exceptions)), - }, - TsType::TypeLit(lit) => TsType::TypeLit(TsTypeLit { - members: lit - .members - .iter() - .map(|t| TsTypeElement { - key: t.key.clone(), - optional: t.optional, - type_ann: t.type_ann.clone().prefix_type_refs(prefix, exceptions), - comments: t.comments.clone(), - }) - .collect(), - }), - TsType::Intersection(tv) => TsType::Intersection( - tv.iter() - .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) - .collect(), - ), - TsType::Union(tv) => TsType::Union( - tv.iter() - .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) - .collect(), - ), - _ => self, - } - } - - pub fn type_refs(&self, type_refs: &mut Vec<(String, Vec)>) { - match self { - TsType::Array(t) | TsType::Option(t, _) => t.type_refs(type_refs), - TsType::Tuple(tv) | TsType::Union(tv) | TsType::Intersection(tv) => { - tv.iter().for_each(|t| t.type_refs(type_refs)) - } - TsType::Ref { name, type_params } => { - type_refs.push((name.clone(), type_params.clone())); - type_params - .iter() - .for_each(|t| t.clone().type_refs(type_refs)); - } - TsType::Fn { params, type_ann } => { - params.iter().for_each(|t| t.clone().type_refs(type_refs)); - type_ann.type_refs(type_refs); - } - TsType::TypeLit(lit) => { - lit.members.iter().for_each(|t| { - t.type_ann.type_refs(type_refs); - }); - } - _ => {} - } - } -} - -fn parse_len(expr: &syn::Expr) -> Option { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Int(lit_int), - .. - }) = expr - { - lit_int.base10_parse::().ok() - } else { - None - } -} - -fn is_js_ident(string: &str) -> bool { - !string.is_empty() - && !string.starts_with(|c: char| c.is_ascii_digit()) - && !string.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '$') -} - -impl TsTypeElement { - pub fn to_string_with_indent(&self, indent: usize) -> String { - let out = self.to_string(); - let indent_str = " ".repeat(indent); - out.split("\n") - .map(|line| format!("{}{}", indent_str, line)) - .collect::>() - .join("\n") - } -} - -impl Display for TsTypeElement { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let key = &self.key; - let type_ann = &self.type_ann; - - let optional_ann = if self.optional { "?" } else { "" }; - - write_doc_comments(f, &self.comments)?; - - if is_js_ident(key) { - write!(f, "{key}{optional_ann}: {type_ann}") - } else { - write!(f, "\"{key}\"{optional_ann}: {type_ann}") - } - } -} - -impl Display for TsTypeLit { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let members = self - .members - .iter() - .map(|elem| elem.to_string()) - .collect::>() - .join("; "); - - if members.is_empty() { - write!(f, "{{}}") - } else { - write!(f, "{{ {members} }}") - } - } -} - -impl Display for TsType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TsType::Keyword(kind) => { - let ty = format!("{:?}", kind).to_lowercase(); - write!(f, "{ty}") - } - - TsType::Lit(lit) => { - write!(f, "\"{lit}\"") - } - - TsType::Array(elem) => match elem.as_ref() { - TsType::Union(_) | TsType::Intersection(_) | &TsType::Option(_, _) => { - write!(f, "({elem})[]") - } - _ => write!(f, "{elem}[]"), - }, - - TsType::Tuple(elems) => { - let elems = elems - .iter() - .map(|elem| elem.to_string()) - .collect::>() - .join(", "); - - write!(f, "[{elems}]") - } - - TsType::Ref { name, type_params } => { - let params = type_params - .iter() - .map(|param| param.to_string()) - .collect::>() - .join(", "); - - if params.is_empty() { - write!(f, "{name}") - } else { - write!(f, "{name}<{params}>") - } - } - - TsType::Fn { params, type_ann } => { - let params = params - .iter() - .enumerate() - .map(|(i, param)| format!("arg{i}: {param}")) - .collect::>() - .join(", "); - - write!(f, "({params}) => {type_ann}") - } - - TsType::Option(elem, null) => { - write!(f, "{elem} | {}", null.to_type()) - } - - TsType::TypeLit(type_lit) => { - write!(f, "{type_lit}") - } - - TsType::Intersection(types) => { - if types.len() == 1 { - let ty = &types[0]; - return write!(f, "{ty}"); - } - - let types = types - .iter() - .map(|ty| match ty { - TsType::Union(_) => format!("({ty})"), - TsType::TypeLit(tl) => { - // Intersections are formatted as single lines, so we need to remove - // any comments as they are multi-line and will break the formatting. - let mut copy = tl.clone(); - copy.members.iter_mut().for_each(|elem| { - elem.comments = vec![]; - }); - copy.to_string() - } - _ => ty.to_string(), - }) - .collect::>() - .join(" & "); - - write!(f, "{types}") - } - - TsType::Union(types) => { - if types.len() == 1 { - let ty = &types[0]; - return write!(f, "{ty}"); - } - - let types = types - .iter() - .map(|ty| match ty { - TsType::Intersection(_) => format!("({ty})"), - _ => ty.to_string(), - }) - .collect::>() - .join(" | "); - - write!(f, "{types}") - } - - TsType::Override { type_override, .. } => f.write_str(type_override), - } - } -} - -#[cfg(test)] -mod tests { - use crate::attrs::TypeGenerationConfig; - - use super::TsType; - - macro_rules! assert_ts { - ($config:expr, $( $t:ty )|* , $expected:expr) => { - $({ - let ty: syn::Type = syn::parse_quote!($t); - let ts_type = TsType::from_syn_type(&$config, &ty); - assert_eq!(ts_type.to_string(), $expected); - })* - }; - } - - #[test] - fn test_basic_types() { - let config = TypeGenerationConfig::default(); - if cfg!(feature = "js") { - assert_ts!(config, (), "undefined"); - assert_ts!(config, u128 | i128, "bigint"); - assert_ts!(config, HashMap | BTreeMap, "Map"); - assert_ts!(config, Option, "number | undefined"); - assert_ts!(config, Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | undefined)[]"); - } else { - assert_ts!(config, (), "null"); - assert_ts!(config, u128 | i128, "number"); - assert_ts!(config, HashMap | BTreeMap, "Record"); - assert_ts!(config, Option, "number | null"); - assert_ts!(config, Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | null)[]"); - assert_ts!(config, ByteBuf, "number[]"); - } - - assert_ts!( - config, - u8 | u16 | u32 | u64 | usize | i8 | i16 | i32 | i64 | isize | f32 | f64, - "number" - ); - assert_ts!(config, String | str | char | Path | PathBuf, "string"); - assert_ts!(config, bool, "boolean"); - assert_ts!(config, Box | Rc | Arc | Cell | RefCell | Cow<'a, i32>, "number"); - assert_ts!(config, Vec | VecDeque | LinkedList | &'a [i32], "number[]"); - assert_ts!(config, HashSet | BTreeSet, "number[]"); - - assert_ts!(config, Result, "{ Ok: number } | { Err: string }"); - assert_ts!(config, dyn Fn(String, f64) | dyn FnOnce(String, f64) | dyn FnMut(String, f64), "(arg0: string, arg1: number) => void"); - assert_ts!(config, dyn Fn(String) -> i32 | dyn FnOnce(String) -> i32 | dyn FnMut(String) -> i32, "(arg0: string) => number"); - - assert_ts!(config, (i32), "number"); - assert_ts!(config, (i32, String, bool), "[number, string, boolean]"); - - assert_ts!(config, [i32; 4], "[number, number, number, number]"); - assert_ts!( - config, - [i32; 16], - format!("[{}]", ["number"; 16].join(", ")) - ); - assert_ts!(config, [i32; 17], "number[]"); - assert_ts!(config, [i32; 1 + 1], "number[]"); - - assert_ts!(config, Duration, "{ secs: number; nanos: number }"); - assert_ts!( - config, - SystemTime, - "{ secs_since_epoch: number; nanos_since_epoch: number }" - ); - - assert_ts!(config, Range, "{ start: number; end: number }"); - assert_ts!( - config, - Range<&'static str>, - "{ start: string; end: string }" - ); - assert_ts!( - config, - RangeInclusive, - "{ start: number; end: number }" - ); - } -} diff --git a/tsify-next-macros/src/typescript/basic.rs b/tsify-next-macros/src/typescript/basic.rs new file mode 100644 index 0000000..42ec200 --- /dev/null +++ b/tsify-next-macros/src/typescript/basic.rs @@ -0,0 +1,156 @@ +use std::fmt::Display; + +use crate::{attrs::TypeGenerationConfig, comments::write_doc_comments}; + +use super::TsType; + +/// Built-in Typescript types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TsKeywordTypeKind { + /// The `number` type. + Number, + /// The `bigint` type. + Bigint, + /// The `boolean` type. + Boolean, + /// The `string` type. + String, + /// The `void` type. + Void, + /// The `undefined` type. + Undefined, + /// The `null` type. + Null, + /// The `never` type. + Never, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TsTypeElement { + pub key: String, + pub type_ann: TsType, + pub optional: bool, + pub comments: Vec, +} + +impl TsTypeElement { + pub fn to_string_with_indent(&self, indent: usize) -> String { + let out = self.to_string(); + let indent_str = " ".repeat(indent); + out.split('\n') + .map(|line| format!("{}{}", indent_str, line)) + .collect::>() + .join("\n") + } +} + +impl Display for TsTypeElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let key = &self.key; + let type_ann = &self.type_ann; + + let optional_ann = if self.optional { "?" } else { "" }; + + write_doc_comments(f, &self.comments)?; + + if is_js_ident(key) { + write!(f, "{key}{optional_ann}: {type_ann}") + } else { + write!(f, "\"{key}\"{optional_ann}: {type_ann}") + } + } +} + +impl From for TsTypeLit { + fn from(m: TsTypeElement) -> Self { + TsTypeLit { members: vec![m] } + } +} + +impl From for TsType { + fn from(m: TsTypeElement) -> Self { + TsType::TypeLit(m.into()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TsTypeLit { + pub members: Vec, +} + +impl TsTypeLit { + pub fn get_mut(&mut self, key: &String) -> Option<&mut TsTypeElement> { + self.members.iter_mut().find(|member| &member.key == key) + } + + pub fn and(self, other: Self) -> Self { + let init = TsTypeLit { members: vec![] }; + + self.members + .into_iter() + .chain(other.members) + .fold(init, |mut acc, m| { + if let Some(acc_m) = acc.get_mut(&m.key) { + let mut tmp = TsType::NULL; + std::mem::swap(&mut acc_m.type_ann, &mut tmp); + acc_m.type_ann = tmp.and(m.type_ann); + } else { + acc.members.push(m) + } + + acc + }) + } +} + +impl From for TsType { + fn from(lit: TsTypeLit) -> Self { + TsType::TypeLit(lit) + } +} + +impl Display for TsTypeLit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let members = self + .members + .iter() + .map(|elem| elem.to_string()) + .collect::>() + .join("; "); + + if members.is_empty() { + write!(f, "{{}}") + } else { + write!(f, "{{ {members} }}") + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NullType { + Null, + Undefined, +} + +impl NullType { + pub const fn new(config: &TypeGenerationConfig) -> Self { + if cfg!(feature = "js") && !config.missing_as_null { + Self::Undefined + } else { + Self::Null + } + } + + pub const fn to_type(&self) -> TsType { + match self { + Self::Null => TsType::NULL, + Self::Undefined => TsType::UNDEFINED, + } + } +} + +fn is_js_ident(string: &str) -> bool { + !string.is_empty() + && !string.starts_with(|c: char| c.is_ascii_digit()) + && !string.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '$') +} diff --git a/tsify-next-macros/src/typescript/mod.rs b/tsify-next-macros/src/typescript/mod.rs new file mode 100644 index 0000000..5cb9efc --- /dev/null +++ b/tsify-next-macros/src/typescript/mod.rs @@ -0,0 +1,7 @@ +mod basic; +mod ts_type; +mod ts_type_display; +mod ts_type_from_name; + +pub use basic::*; +pub use ts_type::*; diff --git a/tsify-next-macros/src/typescript/ts_type.rs b/tsify-next-macros/src/typescript/ts_type.rs new file mode 100644 index 0000000..634d71f --- /dev/null +++ b/tsify-next-macros/src/typescript/ts_type.rs @@ -0,0 +1,443 @@ +use std::collections::HashSet; + +use serde_derive_internals::{ast::Style, attr::TagType}; + +use crate::attrs::TypeGenerationConfig; + +use super::{NullType, TsKeywordTypeKind, TsTypeElement, TsTypeLit}; + +/// A Typescript type +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TsType { + /// A keyword type like `number`, `string`, etc. + Keyword(TsKeywordTypeKind), + /// A literal type like `"foo"`, `42`, etc. + Lit(String), + /// An array type like `number[]`, `(number | string)[]`, etc. + Array(Box), + /// A tuple type like `[number, string]`, `[number, string, boolean]`, etc. + Tuple(Vec), + /// An optional type along with how a missing value is represented (i.e., as `undefined` or `null`). + Option(Box, NullType), + /// A reference to a type like `Foo`, `Bar`, etc. + Ref { + name: String, + type_params: Vec, + }, + /// A function type like `(arg0: string, arg1: number) => void` + Fn { + params: Vec, + type_ann: Box, + }, + /// A type literal like `{ foo: number; bar: string; }` + TypeLit(TsTypeLit), + /// An intersection type like `number & string`, `(number & string) & boolean`, etc. + Intersection(Vec), + /// A union type like `number | string`, `(number | string) | boolean`, etc. + Union(Vec), + /// Explicitly specified type + Override { + type_override: String, + type_params: Vec, + }, +} + +impl From for TsType { + fn from(kind: TsKeywordTypeKind) -> Self { + Self::Keyword(kind) + } +} + +impl TsType { + pub const NUMBER: TsType = TsType::Keyword(TsKeywordTypeKind::Number); + pub const BIGINT: TsType = TsType::Keyword(TsKeywordTypeKind::Bigint); + pub const BOOLEAN: TsType = TsType::Keyword(TsKeywordTypeKind::Boolean); + pub const STRING: TsType = TsType::Keyword(TsKeywordTypeKind::String); + pub const VOID: TsType = TsType::Keyword(TsKeywordTypeKind::Void); + pub const UNDEFINED: TsType = TsType::Keyword(TsKeywordTypeKind::Undefined); + pub const NULL: TsType = TsType::Keyword(TsKeywordTypeKind::Null); + pub const NEVER: TsType = TsType::Keyword(TsKeywordTypeKind::Never); + + pub const fn nullish(config: &TypeGenerationConfig) -> Self { + NullType::new(config).to_type() + } + + pub const fn empty_type_lit() -> Self { + Self::TypeLit(TsTypeLit { members: vec![] }) + } + + pub fn is_ref(&self) -> bool { + matches!(self, Self::Ref { .. }) + } + + pub fn and(self, other: Self) -> Self { + match (self, other) { + (TsType::TypeLit(x), TsType::TypeLit(y)) => x.and(y).into(), + (TsType::Intersection(x), TsType::Intersection(y)) => { + let mut vec = Vec::with_capacity(x.len() + y.len()); + vec.extend(x); + vec.extend(y); + TsType::Intersection(vec) + } + (TsType::Intersection(x), y) => { + let mut vec = Vec::with_capacity(x.len() + 1); + vec.extend(x); + vec.push(y); + TsType::Intersection(vec) + } + (x, TsType::Intersection(y)) => { + let mut vec = Vec::with_capacity(y.len() + 1); + vec.push(x); + vec.extend(y); + TsType::Intersection(vec) + } + (x, y) => TsType::Intersection(vec![x, y]), + } + } + + /// Convert a `syn::Type` to a `TsType` + pub fn from_syn_type(config: &TypeGenerationConfig, ty: &syn::Type) -> Self { + use syn::Type::*; + use syn::{ + TypeArray, TypeBareFn, TypeGroup, TypeImplTrait, TypeParamBound, TypeParen, TypePath, + TypeReference, TypeSlice, TypeTraitObject, TypeTuple, + }; + + match ty { + Array(TypeArray { elem, len, .. }) => { + let elem = Self::from_syn_type(config, elem); + let len = parse_len(len); + + match len { + Some(len) if len <= 16 => Self::Tuple(vec![elem; len]), + _ => Self::Array(Box::new(elem)), + } + } + + Slice(TypeSlice { elem, .. }) => { + Self::Array(Box::new(Self::from_syn_type(config, elem))) + } + + Reference(TypeReference { elem, .. }) + | Paren(TypeParen { elem, .. }) + | Group(TypeGroup { elem, .. }) => Self::from_syn_type(config, elem), + + BareFn(TypeBareFn { inputs, output, .. }) => { + let params = inputs + .iter() + .map(|arg| Self::from_syn_type(config, &arg.ty)) + .collect(); + + let type_ann = if let syn::ReturnType::Type(_, ty) = output { + Self::from_syn_type(config, ty) + } else { + TsType::VOID + }; + + Self::Fn { + params, + type_ann: Box::new(type_ann), + } + } + + Tuple(TypeTuple { elems, .. }) => { + if elems.is_empty() { + TsType::nullish(config) + } else { + let elems = elems + .iter() + .map(|ty| Self::from_syn_type(config, ty)) + .collect(); + Self::Tuple(elems) + } + } + + Path(TypePath { path, .. }) => Self::from_path(config, path).unwrap_or(TsType::NEVER), + + TraitObject(TypeTraitObject { bounds, .. }) + | ImplTrait(TypeImplTrait { bounds, .. }) => { + let elems = bounds + .iter() + .filter_map(|t| match t { + TypeParamBound::Trait(t) => Self::from_path(config, &t.path), + _ => None, // skip lifetime etc. + }) + .collect(); + + Self::Intersection(elems) + } + + Ptr(_) | Infer(_) | Macro(_) | Never(_) | Verbatim(_) => TsType::NEVER, + + _ => TsType::NEVER, + } + } + + /// Convert a `syn::Path` to a `TsType`. For example `core::option::Option` would be + /// converted to `Self::Option(number)`. + fn from_path(config: &TypeGenerationConfig, path: &syn::Path) -> Option { + path.segments + .last() + .map(|segment| Self::from_terminal_path_segment(config, segment)) + } + + /// Convert a `syn::PathSegment` to a `TsType`. For example `Option` would be converted to + /// `Self::Option(number)`. + fn from_terminal_path_segment( + config: &TypeGenerationConfig, + segment: &syn::PathSegment, + ) -> Self { + let name = segment.ident.to_string(); + + let (args, output) = match &segment.arguments { + syn::PathArguments::AngleBracketed(path) => { + let args = path + .args + .iter() + .filter_map(|p| match p { + syn::GenericArgument::Type(t) => Some(t), + syn::GenericArgument::AssocType(t) => Some(&t.ty), + _ => None, + }) + .collect(); + + (args, None) + } + + syn::PathArguments::Parenthesized(path) => { + let args = path.inputs.iter().collect(); + + let output = match &path.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, tp) => Some(tp.as_ref()), + }; + + (args, output) + } + + syn::PathArguments::None => (vec![], None), + }; + + Self::from_name(config, &name, args, output) + } + + pub fn with_tag_type( + self, + config: &TypeGenerationConfig, + name: String, + style: Style, + tag_type: &TagType, + ) -> Self { + let type_ann = self; + + match tag_type { + TagType::External => { + if matches!(style, Style::Unit) { + TsType::Lit(name) + } else { + TsTypeElement { + key: name, + type_ann, + optional: false, + comments: vec![], + } + .into() + } + } + TagType::Internal { tag } => { + if type_ann == TsType::nullish(config) { + let tag_field: TsType = TsTypeElement { + key: tag.clone(), + type_ann: TsType::Lit(name), + optional: false, + comments: vec![], + } + .into(); + + tag_field + } else { + let tag_field: TsType = TsTypeElement { + key: tag.clone(), + type_ann: TsType::Lit(name), + optional: false, + comments: vec![], + } + .into(); + + tag_field.and(type_ann) + } + } + TagType::Adjacent { tag, content } => { + let tag_field = TsTypeElement { + key: tag.clone(), + type_ann: TsType::Lit(name), + optional: false, + comments: vec![], + }; + + if matches!(style, Style::Unit) { + tag_field.into() + } else { + let content_field = TsTypeElement { + key: content.clone(), + type_ann, + optional: false, + comments: vec![], + }; + + TsTypeLit { + members: vec![tag_field, content_field], + } + .into() + } + } + TagType::None => type_ann, + } + } + + pub fn visit<'a, F: FnMut(&'a TsType)>(&'a self, f: &mut F) { + f(self); + + match self { + TsType::Ref { type_params, .. } => { + type_params.iter().for_each(|t| t.visit(f)); + } + TsType::Array(elem) => elem.visit(f), + TsType::Tuple(elems) => { + elems.iter().for_each(|t| t.visit(f)); + } + TsType::Option(t, _) => t.visit(f), + TsType::Fn { params, type_ann } => { + params + .iter() + .chain(Some(type_ann.as_ref())) + .for_each(|t| t.visit(f)); + } + TsType::TypeLit(TsTypeLit { members }) => { + members.iter().for_each(|m| m.type_ann.visit(f)); + } + TsType::Intersection(tys) | TsType::Union(tys) => { + tys.iter().for_each(|t| t.visit(f)); + } + TsType::Keyword(_) | TsType::Lit(_) | TsType::Override { .. } => (), + } + } + + pub fn type_ref_names(&self) -> HashSet<&String> { + let mut set: HashSet<&String> = HashSet::new(); + + self.visit(&mut |ty: &TsType| match ty { + TsType::Ref { name, .. } => { + set.insert(name); + } + TsType::Override { type_params, .. } => set.extend(type_params), + _ => (), + }); + + set + } + + pub fn prefix_type_refs(self, prefix: &String, exceptions: &Vec) -> Self { + match self { + TsType::Array(t) => TsType::Array(Box::new(t.prefix_type_refs(prefix, exceptions))), + TsType::Tuple(tv) => TsType::Tuple( + tv.iter() + .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) + .collect(), + ), + TsType::Option(t, null) => { + TsType::Option(Box::new(t.prefix_type_refs(prefix, exceptions)), null) + } + TsType::Ref { name, type_params } => { + if exceptions.contains(&name) { + TsType::Ref { + name, + type_params: type_params + .iter() + .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) + .collect(), + } + } else { + TsType::Ref { + name: format!("{}{}", prefix, name), + type_params: type_params + .iter() + .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) + .collect(), + } + } + } + TsType::Fn { params, type_ann } => TsType::Fn { + params: params + .iter() + .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) + .collect(), + type_ann: Box::new(type_ann.prefix_type_refs(prefix, exceptions)), + }, + TsType::TypeLit(lit) => TsType::TypeLit(TsTypeLit { + members: lit + .members + .iter() + .map(|t| TsTypeElement { + key: t.key.clone(), + optional: t.optional, + type_ann: t.type_ann.clone().prefix_type_refs(prefix, exceptions), + comments: t.comments.clone(), + }) + .collect(), + }), + TsType::Intersection(tv) => TsType::Intersection( + tv.iter() + .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) + .collect(), + ), + TsType::Union(tv) => TsType::Union( + tv.iter() + .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) + .collect(), + ), + _ => self, + } + } + + pub fn type_refs(&self, type_refs: &mut Vec<(String, Vec)>) { + match self { + TsType::Array(t) | TsType::Option(t, _) => t.type_refs(type_refs), + TsType::Tuple(tv) | TsType::Union(tv) | TsType::Intersection(tv) => { + tv.iter().for_each(|t| t.type_refs(type_refs)) + } + TsType::Ref { name, type_params } => { + type_refs.push((name.clone(), type_params.clone())); + type_params + .iter() + .for_each(|t| t.clone().type_refs(type_refs)); + } + TsType::Fn { params, type_ann } => { + params.iter().for_each(|t| t.clone().type_refs(type_refs)); + type_ann.type_refs(type_refs); + } + TsType::TypeLit(lit) => { + lit.members.iter().for_each(|t| { + t.type_ann.type_refs(type_refs); + }); + } + _ => {} + } + } +} + +fn parse_len(expr: &syn::Expr) -> Option { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Int(lit_int), + .. + }) = expr + { + lit_int.base10_parse::().ok() + } else { + None + } +} + +#[cfg(test)] +#[path = "ts_type.test.rs"] +mod test; diff --git a/tsify-next-macros/src/typescript/ts_type.test.rs b/tsify-next-macros/src/typescript/ts_type.test.rs new file mode 100644 index 0000000..20f976b --- /dev/null +++ b/tsify-next-macros/src/typescript/ts_type.test.rs @@ -0,0 +1,78 @@ +use crate::attrs::TypeGenerationConfig; + +use super::TsType; + +macro_rules! assert_ts { + ($config:expr, $( $t:ty )|* , $expected:expr) => { + $({ + let ty: syn::Type = syn::parse_quote!($t); + let ts_type = TsType::from_syn_type(&$config, &ty); + assert_eq!(ts_type.to_string(), $expected); + })* + }; + } + +#[test] +fn test_basic_types() { + let config = TypeGenerationConfig::default(); + if cfg!(feature = "js") { + assert_ts!(config, (), "undefined"); + assert_ts!(config, u128 | i128, "bigint"); + assert_ts!(config, HashMap | BTreeMap, "Map"); + assert_ts!(config, Option, "number | undefined"); + assert_ts!(config, Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | undefined)[]"); + } else { + assert_ts!(config, (), "null"); + assert_ts!(config, u128 | i128, "number"); + assert_ts!(config, HashMap | BTreeMap, "Record"); + assert_ts!(config, Option, "number | null"); + assert_ts!(config, Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | null)[]"); + assert_ts!(config, ByteBuf, "number[]"); + } + + assert_ts!( + config, + u8 | u16 | u32 | u64 | usize | i8 | i16 | i32 | i64 | isize | f32 | f64, + "number" + ); + assert_ts!(config, String | str | char | Path | PathBuf, "string"); + assert_ts!(config, bool, "boolean"); + assert_ts!(config, Box | Rc | Arc | Cell | RefCell | Cow<'a, i32>, "number"); + assert_ts!(config, Vec | VecDeque | LinkedList | &'a [i32], "number[]"); + assert_ts!(config, HashSet | BTreeSet, "number[]"); + + assert_ts!(config, Result, "{ Ok: number } | { Err: string }"); + assert_ts!(config, dyn Fn(String, f64) | dyn FnOnce(String, f64) | dyn FnMut(String, f64), "(arg0: string, arg1: number) => void"); + assert_ts!(config, dyn Fn(String) -> i32 | dyn FnOnce(String) -> i32 | dyn FnMut(String) -> i32, "(arg0: string) => number"); + + assert_ts!(config, (i32), "number"); + assert_ts!(config, (i32, String, bool), "[number, string, boolean]"); + + assert_ts!(config, [i32; 4], "[number, number, number, number]"); + assert_ts!( + config, + [i32; 16], + format!("[{}]", ["number"; 16].join(", ")) + ); + assert_ts!(config, [i32; 17], "number[]"); + assert_ts!(config, [i32; 1 + 1], "number[]"); + + assert_ts!(config, Duration, "{ secs: number; nanos: number }"); + assert_ts!( + config, + SystemTime, + "{ secs_since_epoch: number; nanos_since_epoch: number }" + ); + + assert_ts!(config, Range, "{ start: number; end: number }"); + assert_ts!( + config, + Range<&'static str>, + "{ start: string; end: string }" + ); + assert_ts!( + config, + RangeInclusive, + "{ start: number; end: number }" + ); +} diff --git a/tsify-next-macros/src/typescript/ts_type_display.rs b/tsify-next-macros/src/typescript/ts_type_display.rs new file mode 100644 index 0000000..64a6938 --- /dev/null +++ b/tsify-next-macros/src/typescript/ts_type_display.rs @@ -0,0 +1,115 @@ +use std::fmt::Display; + +use super::TsType; + +impl Display for TsType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TsType::Keyword(kind) => { + let ty = format!("{:?}", kind).to_lowercase(); + write!(f, "{ty}") + } + + TsType::Lit(lit) => { + write!(f, "\"{lit}\"") + } + + TsType::Array(elem) => match elem.as_ref() { + TsType::Union(_) | TsType::Intersection(_) | &TsType::Option(_, _) => { + write!(f, "({elem})[]") + } + _ => write!(f, "{elem}[]"), + }, + + TsType::Tuple(elems) => { + let elems = elems + .iter() + .map(|elem| elem.to_string()) + .collect::>() + .join(", "); + + write!(f, "[{elems}]") + } + + TsType::Ref { name, type_params } => { + let params = type_params + .iter() + .map(|param| param.to_string()) + .collect::>() + .join(", "); + + if params.is_empty() { + write!(f, "{name}") + } else { + write!(f, "{name}<{params}>") + } + } + + TsType::Fn { params, type_ann } => { + let params = params + .iter() + .enumerate() + .map(|(i, param)| format!("arg{i}: {param}")) + .collect::>() + .join(", "); + + write!(f, "({params}) => {type_ann}") + } + + TsType::Option(elem, null) => { + write!(f, "{elem} | {}", null.to_type()) + } + + TsType::TypeLit(type_lit) => { + write!(f, "{type_lit}") + } + + TsType::Intersection(types) => { + if types.len() == 1 { + let ty = &types[0]; + return write!(f, "{ty}"); + } + + let types = types + .iter() + .map(|ty| match ty { + TsType::Union(_) => format!("({ty})"), + TsType::TypeLit(tl) => { + // Intersections are formatted as single lines, so we need to remove + // any comments as they are multi-line and will break the formatting. + let mut copy = tl.clone(); + copy.members.iter_mut().for_each(|elem| { + elem.comments = vec![]; + }); + copy.to_string() + } + _ => ty.to_string(), + }) + .collect::>() + .join(" & "); + + write!(f, "{types}") + } + + TsType::Union(types) => { + if types.len() == 1 { + let ty = &types[0]; + return write!(f, "{ty}"); + } + + let types = types + .iter() + .map(|ty| match ty { + TsType::Intersection(_) => format!("({ty})"), + _ => ty.to_string(), + }) + .collect::>() + .join(" | "); + + write!(f, "{types}") + } + + TsType::Override { type_override, .. } => f.write_str(type_override), + } + } +} diff --git a/tsify-next-macros/src/typescript/ts_type_from_name.rs b/tsify-next-macros/src/typescript/ts_type_from_name.rs new file mode 100644 index 0000000..fa68a73 --- /dev/null +++ b/tsify-next-macros/src/typescript/ts_type_from_name.rs @@ -0,0 +1,156 @@ +use crate::attrs::TypeGenerationConfig; + +use super::{NullType, TsType, TsTypeElement, TsTypeLit}; + +/// Create a type literal with the given key-value pairs. +/// E.g. `type_lit! { key1: type1; key2: type2 }` will create a type literal with two members +/// named `key1` and `key2` with types `type1` and `type2` respectively. +macro_rules! type_lit { + ($($k: ident: $t: path);* $(;)?) => { + TsType::TypeLit(TsTypeLit { + members: vec![$( + TsTypeElement { + key: stringify!($k).to_string(), + type_ann: $t, + optional: false, + comments: vec![], + } + ),*], + }) + }; +} + +impl TsType { + /// Create a `TsType` from a stringified Rust identifier. + pub fn from_name( + config: &TypeGenerationConfig, + ident: &str, + args: Vec<&syn::Type>, + fn_output: Option<&syn::Type>, + ) -> Self { + match ident { + "u8" | "u16" | "u32" | "i8" | "i16" | "i32" | "f64" | "f32" => Self::NUMBER, + + "usize" | "isize" | "u64" | "i64" => { + if cfg!(feature = "js") && config.large_number_types_as_bigints { + Self::BIGINT + } else { + Self::NUMBER + } + } + + "u128" | "i128" => { + if cfg!(feature = "js") { + Self::BIGINT + } else { + Self::NUMBER + } + } + + "String" | "str" | "char" | "Path" | "PathBuf" => Self::STRING, + + "bool" => Self::BOOLEAN, + + "Box" | "Cow" | "Rc" | "Arc" | "Cell" | "RefCell" if args.len() == 1 => { + Self::from_syn_type(config, args[0]) + } + + "Vec" | "VecDeque" | "LinkedList" if args.len() == 1 => { + let elem = Self::from_syn_type(config, args[0]); + Self::Array(Box::new(elem)) + } + + "HashMap" | "BTreeMap" if args.len() == 2 => { + let type_params = args + .iter() + .map(|arg| Self::from_syn_type(config, arg)) + .collect(); + + let name = if cfg!(feature = "js") && !config.hashmap_as_object { + "Map" + } else { + "Record" + } + .to_string(); + + Self::Ref { name, type_params } + } + + "HashSet" | "BTreeSet" if args.len() == 1 => { + let elem = Self::from_syn_type(config, args[0]); + Self::Array(Box::new(elem)) + } + + "Option" if args.len() == 1 => Self::Option( + Box::new(Self::from_syn_type(config, args[0])), + NullType::new(config), + ), + + "ByteBuf" => { + if cfg!(feature = "js") { + Self::Ref { + name: String::from("Uint8Array"), + type_params: vec![], + } + } else { + Self::Array(Box::new(Self::NUMBER)) + } + } + + "Result" if args.len() == 2 => { + let arg0 = Self::from_syn_type(config, args[0]); + let arg1 = Self::from_syn_type(config, args[1]); + + let ok = type_lit! { Ok: arg0 }; + let err = type_lit! { Err: arg1 }; + + Self::Union(vec![ok, err]) + } + + "Duration" => type_lit! { + secs: Self::NUMBER; + nanos: Self::NUMBER; + }, + + "SystemTime" => type_lit! { + secs_since_epoch: Self::NUMBER; + nanos_since_epoch: Self::NUMBER; + }, + + "Range" | "RangeInclusive" => { + let start = Self::from_syn_type(config, args[0]); + let end = start.clone(); + + type_lit! { + start: start; + end: end; + } + } + + "Fn" | "FnOnce" | "FnMut" => { + let params = args + .into_iter() + .map(|ty| Self::from_syn_type(config, ty)) + .collect(); + let type_ann = fn_output + .map(|ty| Self::from_syn_type(config, ty)) + .unwrap_or_else(|| TsType::VOID); + + Self::Fn { + params, + type_ann: Box::new(type_ann), + } + } + _ => { + let type_params = args + .into_iter() + .map(|ty| Self::from_syn_type(config, ty)) + .collect(); + Self::Ref { + name: config.format_name(ident.to_string()), + type_params, + } + } + } + } +}