Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#42 - Fix recursive types #179

Merged
merged 6 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions macros/src/type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,6 @@ pub fn derive(input: proc_macro::TokenStream) -> syn::Result<proc_macro::TokenSt
});

let comments = &container_attrs.common.doc;
let should_export = match container_attrs.export {
Some(export) => quote!(Some(#export)),
None => quote!(None),
};
let deprecated = container_attrs.common.deprecated_as_tokens(&crate_ref);

let sid = quote!(#crate_ref::internal::construct::sid(#name, concat!("::", module_path!(), ":", line!(), ":", column!())));
Expand Down
95 changes: 46 additions & 49 deletions macros/src/type/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,33 +163,37 @@ pub fn parse_struct(
} else {
let fields = match &data.fields {
Fields::Named(_) => {
let fields = data.fields
.iter()
.map(|field| {
let field_attrs = decode_field_attrs(field)?;
let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty);

let field_ident_str = unraw_raw_ident(field.ident.as_ref().unwrap());

let field_name = match (field_attrs.rename.clone(), container_attrs.rename_all) {
(Some(name), _) => name,
(_, Some(inflection)) => inflection.apply(&field_ident_str).to_token_stream(),
(_, _) => field_ident_str.to_token_stream(),
};

let deprecated = field_attrs.common.deprecated_as_tokens(crate_ref);
let optional = field_attrs.optional;
let flatten = field_attrs.flatten;
let doc = field_attrs.common.doc;

let parent_inline = container_attrs
.inline
.then(|| quote!(true))
.unwrap_or(parent_inline.clone());

let ty = field_attrs.skip.then(|| Ok(quote!(None)))
.unwrap_or_else(|| {
construct_datatype(
let fields =
data.fields
.iter()
.map(|field| {
let field_attrs = decode_field_attrs(field)?;
let field_ty = field_attrs.r#type.as_ref().unwrap_or(&field.ty);

let field_ident_str = unraw_raw_ident(field.ident.as_ref().unwrap());

let field_name =
match (field_attrs.rename.clone(), container_attrs.rename_all) {
(Some(name), _) => name,
(_, Some(inflection)) => {
inflection.apply(&field_ident_str).to_token_stream()
}
(_, _) => field_ident_str.to_token_stream(),
};

let deprecated = field_attrs.common.deprecated_as_tokens(crate_ref);
let optional = field_attrs.optional;
let flatten = field_attrs.flatten;
let doc = field_attrs.common.doc;

let parent_inline = container_attrs
.inline
.then(|| quote!(true))
.unwrap_or(parent_inline.clone());

let ty = field_attrs.skip.then(|| Ok(quote!(None))).unwrap_or_else(
|| {
construct_datatype(
format_ident!("ty"),
field_ty,
&generic_idents,
Expand All @@ -198,20 +202,9 @@ pub fn parse_struct(
).map(|ty| {
let ty = if field_attrs.flatten {
quote! {
#[allow(warnings)]
{
#ty
}

fn validate_flatten<T: #crate_ref::Flatten>() {}
validate_flatten::<#field_ty>();

let mut ty = <#field_ty as #crate_ref::Type>::inline(#crate_ref::DefOpts {
parent_inline: #parent_inline,
type_map: opts.type_map
}, &generics);

ty
#crate_ref::internal::flatten::<#field_ty>(SID, opts.type_map, #parent_inline, &generics)
}
} else {
quote! {
Expand All @@ -225,16 +218,20 @@ pub fn parse_struct(
#ty
})}
})
})?;

Ok(quote!((#field_name.into(), #crate_ref::internal::construct::field(
#optional,
#flatten,
#deprecated,
#doc.into(),
#ty
))))
}).collect::<syn::Result<Vec<TokenStream>>>()?;
},
)?;

Ok(
quote!((#field_name.into(), #crate_ref::internal::construct::field(
#optional,
#flatten,
#deprecated,
#doc.into(),
#ty
))),
)
})
.collect::<syn::Result<Vec<TokenStream>>>()?;

let tag = container_attrs
.tag
Expand Down
9 changes: 1 addition & 8 deletions src/datatype/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::{
borrow::{Borrow, Cow},
collections::BTreeMap,
fmt::Display,
};

Expand All @@ -22,13 +21,7 @@ pub use r#enum::*;
pub use r#struct::*;
pub use tuple::*;

use crate::SpectaID;

/// A map used to store the types "discovered" while exporting a type.
/// You can iterate over this to export all types which the type/s you exported references on.
///
/// [`None`] indicates that the entry is a placeholder. It was reference but we haven't reached it's definition yet.
pub type TypeMap = BTreeMap<SpectaID, Option<NamedDataType>>;
use crate::{SpectaID, TypeMap};

/// Arguments for [`Type::inline`](crate::Type::inline), [`Type::reference`](crate::Type::reference) and [`Type::definition`](crate::Type::definition).
pub struct DefOpts<'a> {
Expand Down
10 changes: 7 additions & 3 deletions src/export/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ pub struct TypesIter {
}

impl Iterator for TypesIter {
type Item = (SpectaID, Option<NamedDataType>);
type Item = (SpectaID, NamedDataType);

fn next(&mut self) -> Option<Self::Item> {
let (k, v) = self.lock.iter().nth(self.index)?;
let (k, v) = self.lock.map.iter().nth(self.index)?;
self.index += 1;
// We have to clone, because we can't invent a lifetime
Some((*k, v.clone()))
Some((
*k,
v.clone()
.expect("specta: `TypesIter` found a type placeholder!"),
))
}
}

Expand Down
51 changes: 23 additions & 28 deletions src/export/ts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::collections::BTreeMap;

use crate::ts::{self, ExportConfig, ExportError};
use crate::{
ts::{self, ExportConfig, ExportError},
TypeMap,
};

use super::get_types;

Expand All @@ -14,46 +17,38 @@ pub fn ts_with_cfg(path: &str, conf: &ExportConfig) -> Result<(), ExportError> {
let mut out = "// This file has been generated by Specta. DO NOT EDIT.\n\n".to_string();

// We sort by name to detect duplicate types BUT also to ensure the output is deterministic. The SID can change between builds so is not suitable for this.
let types = get_types()
.filter(|(_, v)| match v {
Some(_) => true,
None => {
unreachable!("Placeholder type should never be returned from the Specta functions!")
}
})
.collect::<BTreeMap<_, _>>();
let types = get_types().collect::<BTreeMap<_, _>>();

// This is a clone of `detect_duplicate_type_names` but using a `BTreeMap` for deterministic ordering
let mut map = BTreeMap::new();
for (sid, dt) in &types {
match dt {
Some(dt) => {
if let Some(ext) = &dt.ext {
if let Some((existing_sid, existing_impl_location)) =
map.insert(dt.name.clone(), (sid, ext.impl_location))
{
if existing_sid != sid {
return Err(ExportError::DuplicateTypeName(
dt.name.clone(),
ext.impl_location,
existing_impl_location,
));
}
}
if let Some(ext) = &dt.ext {
if let Some((existing_sid, existing_impl_location)) =
map.insert(dt.name.clone(), (sid, ext.impl_location))
{
if existing_sid != sid {
return Err(ExportError::DuplicateTypeName(
dt.name.clone(),
ext.impl_location,
existing_impl_location,
));
}
}
None => unreachable!(),
}
}

for (_, typ) in types.iter() {
out += &ts::export_named_datatype(
conf,
match typ {
Some(v) => v,
None => unreachable!(),
typ,
// TODO: Do this without using private API's
&TypeMap {
map: types
.iter()
.map(|(k, v)| (*k, Some(v.clone())))
.collect::<BTreeMap<_, _>>(),
flatten_stack: vec![],
},
&types,
)?;
out += "\n\n";
}
Expand Down
30 changes: 29 additions & 1 deletion src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub use ctor;
#[cfg(feature = "functions")]
pub use specta_macros::fn_datatype;

use crate::{DataType, Field};
use crate::{DataType, DefOpts, Field, SpectaID, Type, TypeMap};

/// Functions used to construct `crate::datatype` types (they have private fields so can't be constructed directly).
/// We intentionally keep their fields private so we can modify them without a major version bump.
Expand Down Expand Up @@ -190,3 +190,31 @@ pub fn skip_fields_named<'a>(
.into_iter()
.filter_map(|(name, field)| field.ty().map(|ty| (name, (field, ty))))
}

#[track_caller]
pub fn flatten<T: Type>(
sid: SpectaID,
type_map: &mut TypeMap,
parent_inline: bool,
generics: &[DataType],
) -> DataType {
type_map.flatten_stack.push(sid);

#[allow(clippy::panic)]
if type_map.flatten_stack.len() > 25 {
// TODO: Handle this error without panicking
panic!("Type recursion limit exceeded!");
}

let ty = T::inline(
DefOpts {
parent_inline,
type_map,
},
generics,
);

type_map.flatten_stack.pop();

ty
}
2 changes: 1 addition & 1 deletion src/lang/ts/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(crate) fn js_doc_builder(arg: CommentFormatterArgs) -> js_doc::Builder {
}

if let Some(deprecated) = arg.deprecated {
builder.push_deprecated(&deprecated);
builder.push_deprecated(deprecated);
}

builder
Expand Down
2 changes: 1 addition & 1 deletion src/lang/ts/js_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl Builder {
self.value.push_str(part);
}

self.value.push_str("\n");
self.value.push('\n');
}

pub fn push_deprecated(&mut self, typ: &DeprecatedType) {
Expand Down
10 changes: 5 additions & 5 deletions src/lang/ts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub fn export_named_datatype(
)
}

#[allow(clippy::ptr_arg)]
fn inner_comments(
ctx: ExportContext,
deprecated: Option<&DeprecatedType>,
Expand Down Expand Up @@ -220,18 +221,17 @@ pub(crate) fn datatype_inner(ctx: ExportContext, typ: &DataType, type_map: &Type
let dt = if (dt.contains(' ') && !dt.ends_with('}'))
// This is to do with maintaining order of operations.
// Eg `{} | {}` must be wrapped in parens like `({} | {})[]` but `{}` doesn't cause `{}[]` is valid
|| (dt.contains(' ') && (dt.contains("&") || dt.contains("|")))
|| (dt.contains(' ') && (dt.contains('&') || dt.contains('|')))
{
format!("({dt})")
} else {
format!("{dt}")
dt
};

if let Some(length) = def.length {
format!(
"[{}]",
(0..length)
.into_iter()
.map(|_| dt.clone())
.collect::<Vec<_>>()
.join(", ")
Expand Down Expand Up @@ -443,7 +443,7 @@ fn enum_variant_datatype(
Ok(match &fields[..] {
[] => {
// If the actual length is 0, we know `#[serde(skip)]` was not used.
if obj.fields.len() == 0 {
if obj.fields.is_empty() {
Some("[]".to_string())
} else {
// We wanna render `{tag}` not `{tag}: {type}` (where `{type}` is what this function returns)
Expand Down Expand Up @@ -647,7 +647,7 @@ pub(crate) fn sanitise_type_name(ctx: ExportContext, loc: NamedLocation, ident:
return Err(ExportError::ForbiddenName(loc, ctx.export_path(), name));
}

if let Some(first_char) = ident.chars().nth(0) {
if let Some(first_char) = ident.chars().next() {
if !first_char.is_alphabetic() && first_char != '_' {
return Err(ExportError::InvalidName(
loc,
Expand Down
Loading
Loading