use super::*;
fn kind(ty: proc_macro2::TokenStream) -> TypeKind {
lower(ty).expect("in the language").kind
}
fn reason(ty: proc_macro2::TokenStream) -> UnsupportedTypeReason {
lower(ty).expect_err("outside the language").reason
}
#[test]
fn scalars_and_strings() {
assert!(matches!(
kind(quote::quote!(u8)),
TypeKind::Scalar(ScalarKind::U8)
));
assert!(matches!(
kind(quote::quote!(bool)),
TypeKind::Scalar(ScalarKind::Bool)
));
assert!(matches!(
kind(quote::quote!(f64)),
TypeKind::Scalar(ScalarKind::F64)
));
assert!(matches!(kind(quote::quote!(String)), TypeKind::String));
assert!(matches!(kind(quote::quote!(())), TypeKind::Unit));
}
#[test]
fn the_two_string_types_stay_two() {
assert!(matches!(kind(quote::quote!(str)), TypeKind::Str));
assert!(matches!(kind(quote::quote!(String)), TypeKind::String));
for (spelling, owned) in [(quote::quote!(&str), false), (quote::quote!(&String), true)] {
let TypeKind::Ref { mutable, inner, .. } = kind(spelling) else {
panic!("a borrow");
};
assert!(!mutable);
assert_eq!(matches!(inner.kind, TypeKind::String), owned);
assert_eq!(matches!(inner.kind, TypeKind::Str), !owned);
}
}
#[test]
fn the_builtin_generics() {
assert!(matches!(
kind(quote::quote!(Option<u8>)),
TypeKind::Optional(_)
));
assert!(matches!(kind(quote::quote!(Vec<u8>)), TypeKind::Vec(_)));
assert!(matches!(
kind(quote::quote!(Result<u8, Error>)),
TypeKind::Fallible { .. }
));
}
#[test]
fn a_box_is_a_box_until_a_consumer_unwraps_it() {
let ty = lower(quote::quote!(Box<String>)).expect("in the language");
let TypeKind::Boxed(inner) = &ty.kind else {
panic!("a box");
};
assert!(matches!(inner.kind, TypeKind::String));
assert_eq!(tokens(ty.origin.as_syn()), "Box < String >");
assert!(matches!(ty.unwrapped().kind(), TypeKind::String));
let ty = lower(quote::quote!(Option<Box<String>>)).expect("in the language");
let inner = ty.optional_inner().expect("an option");
assert!(matches!(inner.kind, TypeKind::Boxed(_)));
assert!(matches!(inner.unwrapped().kind(), TypeKind::String));
assert_eq!(tokens(inner.origin.as_syn()), "Box < String >");
}
#[test]
fn the_stripped_spelling_is_the_one_that_lowers_to_this_kind() {
for spelling in [
quote::quote!(Box<Option<Sample>>),
quote::quote!(Box<Box<String>>),
quote::quote!(Box<Box<Box<Vec<u8>>>>),
quote::quote!(Box<Cow<'_, [u8]>>),
quote::quote!(Cow<'_, str>),
quote::quote!(Option<Sample>),
] {
let ty = lower(spelling).expect("in the language");
let stripped = ty.stripped_syntax();
assert_eq!(
format!("{:?}", kind(quote::quote!(#stripped))),
format!("{:?}", ty.unwrapped().kind()),
"`{}` strips to `{}`, which must classify identically",
tokens(ty.origin.as_syn()),
tokens(&stripped),
);
}
let wrappers =
|t: proc_macro2::TokenStream| lower(t).expect("in the language").erased_wrappers();
assert_eq!(wrappers(quote::quote!(Box<Box<String>>)), ["Box", "Box"]);
assert_eq!(wrappers(quote::quote!(Box<Cow<'_, [u8]>>)), ["Box", "Cow"]);
assert_eq!(wrappers(quote::quote!(Option<Sample>)), [] as [&str; 0]);
let plain = lower(quote::quote!(Option<Sample>)).expect("in the language");
assert_eq!(
tokens(&plain.stripped_syntax()),
tokens(plain.origin.as_syn())
);
assert_eq!(
tokens(
&lower(quote::quote!(Box<Box<Option<Sample>>>))
.expect("in the language")
.stripped_syntax()
),
"Option < Sample >"
);
}
#[test]
fn a_wrapper_is_found_only_at_the_layer_that_spells_it() {
let outside = lower(quote::quote!(Box<&Vec<Sample>>)).expect("in the language");
assert_eq!(outside.erased_wrappers(), ["Box"]);
let referent = outside.borrow_target().expect("a borrow");
assert_eq!(
referent.erased_wrappers(),
[] as [&str; 0],
"peeling `kind` first reaches a clean `Vec<T>` — the `Box` is only \
visible before the peel"
);
let inside = lower(quote::quote!(&Box<Vec<Sample>>)).expect("in the language");
assert_eq!(
inside.erased_wrappers(),
[] as [&str; 0],
"a reference cannot be peeled as a transparent wrapper"
);
assert_eq!(
inside.borrow_target().expect("a borrow").erased_wrappers(),
["Box"],
"the wrapper is on the referent, after the peel"
);
for ty in [&outside, &inside] {
let TypeKind::Ref { mutable, inner, .. } = ty.unwrapped().kind() else {
panic!("a borrow");
};
assert!(!mutable);
let TypeKind::Vec(elem) = inner.unwrapped().kind() else {
panic!("a run");
};
assert!(matches!(elem.kind, TypeKind::Named { .. }));
}
}
#[test]
fn the_prelude_reaches_every_builtin_by_either_spelling() {
use crate::flat::spelling::Normalization;
for (path, name) in Normalization::PRELUDE {
if *name == "MaybeUninit" {
continue;
}
let bare: proc_macro2::TokenStream = match *name {
"Result" => quote::quote!(Result<u8, Error>),
"String" => quote::quote!(String),
"Cow" => quote::quote!(Cow<'_, [u8]>),
_ => {
let n = quote::format_ident!("{name}");
quote::quote!(#n<u8>)
}
};
let qualified: proc_macro2::TokenStream = {
let p: syn::Path = syn::parse_str(path).expect("a prelude path");
match *name {
"Result" => quote::quote!(#p<u8, Error>),
"String" => quote::quote!(#p),
"Cow" => quote::quote!(#p<'_, [u8]>),
_ => quote::quote!(#p<u8>),
}
};
assert_eq!(
format!("{:?}", kind(bare)),
format!("{:?}", kind(qualified)),
"`{name}` must classify the same as `{path}`"
);
}
assert!(matches!(
kind(quote::quote!(core::option::Option<u8>)),
TypeKind::Optional(_)
));
assert!(matches!(
kind(quote::quote!(alloc::string::String)),
TypeKind::String
));
let TypeKind::Ref { mutable, inner, .. } =
kind(quote::quote!(&mut std::mem::MaybeUninit<Sample>))
else {
panic!("a borrow");
};
assert!(mutable);
assert!(matches!(inner.kind, TypeKind::Uninit(_)));
}
#[test]
fn an_alias_is_a_declaration_not_an_equivalence() {
let items: Vec<syn::Item> = vec![
syn::parse_quote!(
pub type Session = zenoh::Session;
),
syn::parse_quote!(
pub fn by_name(s: &Session) {}
),
syn::parse_quote!(
pub fn by_path(s: &zenoh::Session) {}
),
];
let flat = Flat::builder()
.items(items.into_iter().map(|i| (i, loc())))
.build()
.expect("a refusal is deferred, not fatal");
let f = flat.function("by_name").expect("declared");
let inner = f.params[0].ty.borrow_target().expect("a borrow");
let TypeKind::Named { id, .. } = &inner.kind else {
panic!("a nominal type");
};
assert_eq!(id.name, "Session");
assert!(flat.function("by_path").is_none());
let u = flat.unsupported().next().expect("one refusal");
assert!(matches!(
&*u.error,
ItemError::UnresolvedType { name } if name == "zenoh::Session"
));
assert!(
u.error.to_string().contains("refer to that"),
"the diagnosis must point at the declared name: {}",
u.error
);
let Type::Extern(e) = flat.declared_type("Session").expect("declared") else {
panic!("an extern");
};
assert_eq!(e.target.as_deref(), Some("zenoh :: Session"));
}
#[test]
fn an_alias_never_retypes_a_spelling() {
let flat = Flat::builder()
.items(
vec![
syn::parse_quote!(
pub type Bytes = std::vec::Vec<u8>;
),
syn::parse_quote!(
pub type Small = zenoh::Wrap<4>;
),
syn::parse_quote!(
pub type Big = zenoh::Wrap<8>;
),
syn::parse_quote!(
pub fn strings(xs: std::vec::Vec<String>) {}
),
syn::parse_quote!(
pub fn bytes(xs: std::vec::Vec<u8>) {}
),
syn::parse_quote!(
pub fn by_name(b: Bytes) {}
),
syn::parse_quote!(
pub fn small(w: Small) {}
),
syn::parse_quote!(
pub fn big(w: Big) {}
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("parses");
let param = |name: &str| {
flat.function(name)
.unwrap_or_else(|| panic!("{name} survives"))
.params[0]
.ty
.kind
.clone()
};
for f in ["strings", "bytes"] {
assert!(
matches!(param(f), TypeKind::Vec(_)),
"`{f}`: the grammar's spelling stays canonical"
);
}
for (f, expected) in [("by_name", "Bytes"), ("small", "Small"), ("big", "Big")] {
let TypeKind::Named { id, .. } = param(f) else {
panic!("{f}: a nominal type");
};
assert_eq!(id.name, expected, "{f}");
assert!(matches!(
flat.declared_type(expected).expect(expected),
Type::Extern(_)
));
}
}
#[test]
fn a_qualified_builtin_is_a_named_type() {
assert!(matches!(
kind(quote::quote!(std::option::Option<u8>)),
TypeKind::Optional(_)
));
let element = {
let mut items = fixture_types();
let n = items.len();
items.push(syn::parse_quote!(
pub struct S {
pub f: foreign::Option<u8>,
}
));
parse(items).remove(n)
};
assert!(matches!(
as_unsupported(&element),
ItemError::UnresolvedType { name } if name == "foreign::Option"
));
}
#[test]
fn references() {
assert!(matches!(
kind(quote::quote!(&Sample)),
TypeKind::Ref { mutable: false, .. }
));
assert!(matches!(
kind(quote::quote!(&mut Sample)),
TypeKind::Ref { mutable: true, .. }
));
let TypeKind::Ref { lifetime, .. } = kind(quote::quote!(&'a Sample)) else {
panic!("a borrow");
};
assert_eq!(lifetime.expect("a lifetime").ident, "a");
}
#[test]
fn a_run_of_values_is_read_through_either_spelling() {
assert!(matches!(kind(quote::quote!(Vec<u8>)), TypeKind::Vec(_)));
assert!(matches!(kind(quote::quote!([u8])), TypeKind::Slice(_)));
let TypeKind::Ref { inner, .. } = kind(quote::quote!(&[u8])) else {
panic!("a reference");
};
assert!(matches!(inner.kind, TypeKind::Slice(_)));
for spelling in [
quote::quote!(Vec<u8>),
quote::quote!([u8]),
quote::quote!(Box<Vec<u8>>),
quote::quote!(Cow<'_, [u8]>),
] {
let ty = lower(spelling).expect("in the language");
assert!(
matches!(
ty.sequence_elem().expect("a run").kind(),
TypeKind::Scalar(ScalarKind::U8)
),
"`{}` is a run of `u8`",
tokens(ty.origin.as_syn())
);
}
}
#[test]
fn a_cow_reads_as_what_it_borrows() {
let cow = lower(quote::quote!(Cow<'_, [u8]>)).expect("in the language");
assert_eq!(
format!("{:?}", cow.unwrapped().kind()),
format!("{:?}", kind(quote::quote!([u8]))),
"a byte Cow reads exactly as the byte slice it borrows"
);
let TypeKind::Cow { lifetime, .. } = &cow.kind else {
panic!("a cow");
};
assert_eq!(lifetime.ident, "_");
assert!(matches!(
lower(quote::quote!(Cow<'_, str>))
.expect("in the language")
.unwrapped()
.kind(),
TypeKind::Str
));
assert_eq!(tokens(cow.origin.as_syn()), "Cow < '_ , [u8] >");
let elem = lower(quote::quote!(Cow<'_, [Sample]>))
.expect("in the language")
.sequence_elem()
.expect("a run")
.clone();
assert!(matches!(elem.kind, TypeKind::Named { .. }));
let element = {
let mut items = fixture_types();
let n = items.len();
items.push(syn::parse_quote!(
pub struct S {
pub f: Vec<'a, u8>,
}
));
parse(items).remove(n)
};
assert!(matches!(
as_unsupported(&element),
ItemError::UnresolvedType { name } if name == "Vec"
));
}
#[test]
fn a_cow_takes_a_lifetime_and_a_type_in_that_order() {
for spelling in [quote::quote!(Cow<'_, [u8]>), quote::quote!(Cow<'a, str>)] {
let ty = lower(spelling).expect("in the language");
assert!(matches!(ty.kind, TypeKind::Cow { .. }));
assert_eq!(tokens(&ty.kind().to_syn()), tokens(ty.as_syn()));
}
for spelling in [
quote::quote!(Cow<u8>),
quote::quote!(Cow<u8, 'a>),
quote::quote!(Cow<'a, 'b, u8>),
quote::quote!(Cow<'a, u8, u8>),
] {
let rendered = spelling.to_string();
assert_eq!(
reason(spelling),
UnsupportedTypeReason::WrongGenericArguments {
expected: "Cow<'a, T>"
},
"`{rendered}` is not a `Cow`"
);
}
}
#[test]
fn a_cow_returning_accessor_resolves() {
let flat = Flat::builder()
.items(
vec![
syn::parse_quote!(
pub type ZBytes = zenoh::bytes::ZBytes;
),
syn::parse_quote!(
pub fn zbytes_to_bytes(z: &ZBytes) -> Cow<'_, [u8]> {}
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("parses");
assert_eq!(flat.unsupported().count(), 0, "no longer refused");
let f = flat.function("zbytes_to_bytes").expect("survives");
assert!(matches!(f.ret.kind, TypeKind::Cow { .. }));
assert!(f.ret.sequence_elem().is_some(), "and it reads as a run");
assert_eq!(tokens(f.ret.origin.as_syn()), "Cow < '_ , [u8] >");
}
#[test]
fn a_raw_pointer_is_not_in_the_language() {
assert_eq!(
reason(quote::quote!(*const u8)),
UnsupportedTypeReason::UnsupportedForm
);
assert_eq!(
reason(quote::quote!(*mut Sample)),
UnsupportedTypeReason::UnsupportedForm
);
}
#[test]
fn generic_arguments_are_spelling_only() {
let ty = lower(quote::quote!(Foo<'a, u8>)).expect("in the language");
let TypeKind::Named { id, .. } = &ty.kind else {
panic!("a named type");
};
assert_eq!(id.name, "Foo");
assert_eq!(tokens(ty.origin.as_syn()), "Foo < 'a , u8 >");
assert_eq!(
reason(quote::quote!(Foo<(u8, u8)>)),
UnsupportedTypeReason::UnsupportedTuple
);
}
#[test]
fn the_callback_form() {
let TypeKind::Callback { args } =
kind(quote::quote!(impl Fn(&Sample, u32) + Send + Sync + 'static))
else {
panic!("a callback");
};
assert_eq!(args.len(), 2);
}
#[test]
fn a_callback_must_return_nothing() {
let TypeKind::Callback { args } =
kind(quote::quote!(impl Fn(u32) -> () + Send + Sync + 'static))
else {
panic!("a callback");
};
assert_eq!(args.len(), 1);
for spelling in [
quote::quote!(impl Fn() -> u8 + Send + Sync + 'static),
quote::quote!(impl Fn(u32) -> Sample + Send + Sync + 'static),
quote::quote!(impl Fn() -> Option<u8> + Send + Sync + 'static),
] {
assert_eq!(
reason(spelling),
UnsupportedTypeReason::DisallowedImplTrait,
"a returning callback is refused, not silently truncated"
);
}
}
#[test]
fn types_outside_the_language() {
assert_eq!(
reason(quote::quote!((u8, u8))),
UnsupportedTypeReason::UnsupportedTuple
);
assert_eq!(
reason(quote::quote!(<Holder as Trait>::Assoc)),
UnsupportedTypeReason::AssociatedType
);
assert_eq!(
reason(quote::quote!(Option<u8, u16>)),
UnsupportedTypeReason::WrongGenericArity { expected: 1 }
);
assert_eq!(
reason(quote::quote!(Result<u8>)),
UnsupportedTypeReason::WrongGenericArity { expected: 2 }
);
assert_eq!(
reason(quote::quote!(impl Iterator<Item = u8>)),
UnsupportedTypeReason::DisallowedImplTrait
);
assert_eq!(
reason(quote::quote!(dyn Fn(u8))),
UnsupportedTypeReason::UnsupportedForm
);
assert_eq!(
reason(quote::quote!(!)),
UnsupportedTypeReason::UnsupportedForm
);
}
fn extent_reason(ty: proc_macro2::TokenStream) -> ArrayLenReason {
match reason(ty) {
UnsupportedTypeReason::BadArrayExtent(e) => e.reason,
other => panic!("expected an extent diagnosis, got {other:?}"),
}
}
#[test]
fn extents_outside_the_subgrammar() {
assert_eq!(
extent_reason(quote::quote!([u8; TAG_LEN + 1])),
ArrayLenReason::NotLiteralOrName
);
assert_eq!(
extent_reason(quote::quote!([u8; crate::limits::MAX])),
ArrayLenReason::NotABareName
);
assert_eq!(
extent_reason(quote::quote!([u8; UNMARKED])),
ArrayLenReason::NotAMarkedConst
);
assert_eq!(
extent_reason(quote::quote!(
[u8; const {
let n = 3;
n
}]
)),
ArrayLenReason::NotLiteralOrName
);
assert_eq!(
extent_reason(quote::quote!(
[u8; match 3 {
n => n,
}]
)),
ArrayLenReason::NotLiteralOrName
);
assert_eq!(
extent_reason(quote::quote!([u8; if let n = 3 { n } else { 0 }])),
ArrayLenReason::NotLiteralOrName
);
assert_eq!(
extent_reason(quote::quote!([u8; array_len()])),
ArrayLenReason::NotLiteralOrName
);
assert_eq!(
extent_reason(quote::quote!([u8; 'c'])),
ArrayLenReason::NotAnIntegerLiteral
);
}
#[test]
fn an_extent_may_name_a_const_declared_later() {
let elements = parse(vec![
syn::parse_quote!(
pub struct Marker {
pub tag: [u8; TAG_LEN],
}
),
tag_len_const(),
]);
assert_eq!(
as_struct(&elements[0]).fields[0]
.ty
.array_extent()
.expect("an extent")
.value,
4
);
}
#[test]
fn the_three_extent_projections_are_independent() {
let elements = parse(vec![
syn::parse_quote!(
pub struct Marker {
pub by_const: [u8; TAG_LEN],
pub by_other_const: [u8; ALSO_FOUR],
pub by_literal: [u8; 4],
pub by_hex_literal: [u8; 0x04],
pub longer: [u8; 8],
}
),
tag_len_const(),
syn::parse_quote!(
pub const ALSO_FOUR: usize = 4;
),
]);
let fields = &as_struct(&elements[0]).fields;
let at = |i: usize| fields[i].ty.array_extent().expect("an extent");
let (by_const, by_other_const, by_literal, by_hex, longer) =
(at(0), at(1), at(2), at(3), at(4));
for e in [by_const, by_other_const, by_literal, by_hex] {
assert_eq!(e.value, 4);
}
assert_ne!(longer.value, by_literal.value);
assert_eq!(tokens(by_literal.origin.as_syn()), "4");
assert_eq!(tokens(by_hex.origin.as_syn()), "0x04");
assert_eq!(tokens(by_const.origin.as_syn()), "TAG_LEN");
assert_eq!(
by_const.const_id().expect("a const dependency").name,
"TAG_LEN"
);
assert_eq!(
by_other_const.const_id().expect("a const dependency").name,
"ALSO_FOUR"
);
assert!(by_literal.const_id().is_none());
assert!(by_hex.const_id().is_none());
assert!(by_const.value == by_literal.value && by_const.const_id() != by_literal.const_id());
assert!(
by_literal.value == by_hex.value
&& tokens(by_literal.origin.as_syn()) != tokens(by_hex.origin.as_syn())
);
assert!(
by_const.const_id() != by_other_const.const_id() && by_const.value == by_other_const.value
);
}
#[test]
fn a_computed_const_is_indexed_but_is_not_a_length() {
let elements = parse(vec![
syn::parse_quote!(
pub const COMPUTED: usize = 2 * 2;
),
syn::parse_quote!(
pub struct Marker {
pub tag: [u8; COMPUTED],
}
),
]);
assert_eq!(as_const(&elements[0]).name, "COMPUTED");
match as_unsupported(&elements[1]) {
ItemError::FieldType {
source:
UnsupportedType {
reason: UnsupportedTypeReason::BadArrayExtent(e),
..
},
..
} => assert_eq!(e.reason, ArrayLenReason::ConstIsNotALiteral),
other => panic!("expected an extent diagnosis, got {other}"),
}
}
#[test]
fn struct_shapes() {
let named = parse_one(syn::parse_quote!(
pub struct A {
pub x: u8,
}
));
assert_eq!(as_struct(&named).fields.len(), 1);
let tuple = parse_one(syn::parse_quote!(
pub struct B(SomethingUnexpressible<'_, dyn Trait>);
));
assert_eq!(as_extern(&tuple).name, "B");
let unit = parse_one(syn::parse_quote!(
pub struct C;
));
assert!(
as_struct(&unit).fields.is_empty(),
"an empty product, not a handle"
);
}
#[test]
fn tags_are_declaration_order() {
let element = parse_one(syn::parse_quote!(
pub enum E {
A = 5,
B = 9,
}
));
let e = as_enum(&element);
assert_eq!(
e.values.iter().map(|v| v.index).collect::<Vec<_>>(),
vec![0, 1]
);
assert_eq!(
e.discriminant_values()
.expect("literals")
.into_iter()
.map(|(_, v)| v)
.collect::<Vec<_>>(),
vec![5, 9]
);
}
#[test]
fn the_two_enum_shapes_are_two_entities() {
let fieldless = parse_one(syn::parse_quote!(
pub enum E {
A,
B = 7,
}
));
let e = as_enum(&fieldless);
assert_eq!(e.values.len(), 2);
assert_eq!(e.values[1].discriminant, Some(7));
let empty_groups = parse_one(syn::parse_quote!(
pub enum E {
A,
B(),
C {},
}
));
assert_eq!(as_enum(&empty_groups).values.len(), 3);
let sum = parse_one(syn::parse_quote!(
pub enum E {
A,
B(u32),
C { x: u8 },
}
));
let v = as_variant(&sum);
assert_eq!(v.alternatives.len(), 3);
assert!(v.alternatives[0].is_empty(), "a sum may mix");
assert_eq!(v.alternatives[1].fields.len(), 1);
assert_eq!(
v.alternatives.iter().map(|a| a.index).collect::<Vec<_>>(),
vec![0, 1, 2]
);
let empty = parse_one(syn::parse_quote!(
pub enum E {}
));
assert!(as_enum(&empty).values.is_empty());
}
#[test]
fn field_members_follow_the_addressing() {
let element = parse_one(syn::parse_quote!(
pub enum Reading {
Exact(i64, i64),
Range { low: i64 },
}
));
let v = as_variant(&element);
assert!(matches!(
v.alternatives[0].fields[1].member(),
syn::Member::Unnamed(i) if i.index == 1
));
assert!(matches!(
v.alternatives[1].fields[0].member(),
syn::Member::Named(id) if id == "low"
));
}
#[test]
fn consts_carry_their_type_and_value() {
let element = parse_one(tag_len_const());
let c = as_const(&element);
assert_eq!(c.name, "TAG_LEN");
assert!(matches!(c.ty.kind, TypeKind::Scalar(ScalarKind::Usize)));
assert_eq!(tokens(&c.origin.as_syn().expr), "4");
}
#[test]
fn an_unnamed_const_is_a_guard() {
let elements = parse(vec![
syn::parse_quote!(
const _: () = ();
),
syn::parse_quote!(
const _: () = ();
),
]);
assert!(elements.iter().all(|e| matches!(e, Element::Guard(_))));
assert!(elements.iter().all(|e| e.name().is_none()));
assert!(!elements.iter().any(|e| matches!(e, Element::Constant(_))));
}
#[test]
fn an_unmodelled_item_kind_is_diagnosed() {
let element = parse_one(syn::parse_quote!(
pub union U {
a: u8,
}
));
assert!(element.name().is_some(), "keeps its address");
assert!(matches!(
as_unsupported(&element),
ItemError::UnsupportedItemKind { kind } if *kind == "a union"
));
}
#[test]
fn a_marked_alias_declares_an_extern() {
let element = parse_one(syn::parse_quote!(
pub type Session = zenoh::Session;
));
let e = as_extern(&element);
assert_eq!(e.name, "Session");
assert_eq!(e.target.as_deref(), Some("zenoh :: Session"));
assert_eq!(
tokens(e.origin.as_syn()),
"pub type Session = zenoh :: Session ;"
);
let element = parse_one(syn::parse_quote!(
pub type Duration = std::time::Duration;
));
assert_eq!(
as_extern(&element).target.as_deref(),
Some("std :: time :: Duration")
);
let element = parse_one(syn::parse_quote!(
pub struct Handle(Whatever);
));
let e = as_extern(&element);
assert_eq!(e.name, "Handle");
assert_eq!(e.target, None);
let mut items = fixture_types();
items.push(syn::parse_quote!(
pub type Session = zenoh::Session;
));
let n = items.len();
items.push(syn::parse_quote!(
pub fn session_close(s: Session) {}
));
let elements = parse(items);
assert!(matches!(elements[n], Element::Function(_)));
}
#[test]
fn function_signatures() {
let element = parse_one(syn::parse_quote!(
pub fn put(key: &KeyExpr, payload: Vec<u8>) -> Result<(), Error> {
unimplemented!()
}
));
let f = as_fn(&element);
assert_eq!(f.name, "put");
assert_eq!(
f.params
.iter()
.map(|p| p.name.to_string())
.collect::<Vec<_>>(),
vec!["key", "payload"]
);
assert!(matches!(f.ret.kind, TypeKind::Fallible { .. }));
}
#[test]
fn an_elided_return_is_the_unit() {
for sig in [
quote::quote!(
pub fn f() {}
),
quote::quote!(
pub fn f() -> () {}
),
] {
let element = parse_one(syn::parse_quote!(#sig));
assert!(matches!(as_fn(&element).ret.kind, TypeKind::Unit));
}
}
#[test]
fn function_shapes_outside_the_language() {
let element = parse_one(syn::parse_quote!(
pub async fn ping() {}
));
assert!(matches!(
as_unsupported(&element),
ItemError::UnsupportedAsync
));
assert_eq!(element.name().expect("named"), "ping");
let element = parse_one(syn::parse_quote!(
pub unsafe extern "C" fn log(fmt: u8, ...) {}
));
assert!(matches!(
as_unsupported(&element),
ItemError::UnsupportedVariadic
));
}
#[test]
fn a_generic_parameter_is_outside_the_language() {
let cases: Vec<(syn::Item, &str, &str)> = vec![
(
syn::parse_quote!(
pub struct Wrapper<T> {
pub value: T,
}
),
"T",
"a type parameter",
),
(
syn::parse_quote!(
pub fn first<T>(items: Vec<T>) -> T {
unimplemented!()
}
),
"T",
"a type parameter",
),
(
syn::parse_quote!(
pub enum Either<L, R> {
Left(L),
Right(R),
}
),
"L",
"a type parameter",
),
(
syn::parse_quote!(
pub struct Padded<const N: usize> {
pub value: u8,
}
),
"N",
"a const generic parameter",
),
];
for (item, expected_param, expected_kind) in cases {
let element = parse_one(item);
let ItemError::UnsupportedGenericParam { param, kind } = as_unsupported(&element) else {
panic!(
"expected a generic-parameter diagnosis, got {}",
describe(&element)
);
};
assert_eq!(param, expected_param);
assert_eq!(*kind, expected_kind);
}
}
#[test]
fn a_lifetime_binder_is_accepted() {
let element = parse_one(syn::parse_quote!(
pub struct Borrowed<'a> {
pub key: &'a str,
}
));
let s = as_struct(&element);
assert_eq!(s.fields.len(), 1);
assert_eq!(tokens(&s.fields[0].ty.origin.spell()), "& 'a str");
}
#[test]
fn a_callback_parameter_is_not_a_generic_binder() {
let element = parse_one(syn::parse_quote!(
pub fn for_each(f: impl Fn(u64) + Send + Sync + 'static) {}
));
let func = as_fn(&element);
assert!(matches!(func.params[0].ty.kind, TypeKind::Callback { .. }));
}
#[test]
fn a_receiver_is_not_a_free_function() {
let element = parse_one(syn::parse_quote!(
pub fn get(self) -> u8 {
unimplemented!()
}
));
assert!(matches!(
as_unsupported(&element),
ItemError::UnsupportedReceiver
));
}
#[test]
fn a_parameter_must_be_bound_to_one_name() {
let element = parse_one(syn::parse_quote!(
pub fn f((a, b): (u8, u8)) {}
));
assert!(matches!(
as_unsupported(&element),
ItemError::UnsupportedParamPattern { .. }
));
}
#[test]
fn a_diagnosis_names_the_component() {
let element = parse_one(syn::parse_quote!(
pub fn f(ok: u8, bad: (u8, u8)) {}
));
match as_unsupported(&element) {
ItemError::ParamType { param, source } => {
assert_eq!(param, "bad");
assert_eq!(source.reason, UnsupportedTypeReason::UnsupportedTuple);
}
other => panic!("expected a parameter diagnosis, got {other}"),
}
let element = parse_one(syn::parse_quote!(
pub fn f() -> (u8, u8) {
unimplemented!()
}
));
assert!(matches!(
as_unsupported(&element),
ItemError::ReturnType { .. }
));
let element = parse_one(syn::parse_quote!(
pub enum E {
V { bad: (u8, u8) },
}
));
match as_unsupported(&element) {
ItemError::VariantFieldType { variant, field, .. } => {
assert_eq!(variant, "V");
assert_eq!(field, "bad");
}
other => panic!("expected a variant diagnosis, got {other}"),
}
}
#[test]
fn an_unsupported_item_is_indexed_not_refused() {
let elements = parse(vec![
syn::parse_quote!(
pub fn unusable(pair: (u8, u8)) {}
),
syn::parse_quote!(
pub fn usable(x: u8) {}
),
]);
assert_eq!(elements[0].name().expect("named"), "unusable");
assert!(matches!(elements[0], Element::Unsupported(_)));
assert!(matches!(elements[1], Element::Function(_)));
}
#[test]
fn the_model_is_addressed_by_name() {
let flat = Flat::builder()
.items(
vec![
syn::parse_quote!(
pub type Session = zenoh::Session;
),
syn::parse_quote!(
pub const LIMIT: usize = 4;
),
syn::parse_quote!(
pub fn session_close(s: Session) {}
),
syn::parse_quote!(
pub union U {
a: u8,
}
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("parses");
assert!(flat.function("session_close").is_some());
assert!(flat.declared_type("Session").is_some());
assert!(flat.constant("LIMIT").is_some());
assert_eq!(flat.functions().count(), 1);
assert_eq!(flat.types().count(), 1);
assert_eq!(flat.constants().count(), 1);
assert!(flat.element("U").is_some());
assert!(flat.declared_type("U").is_none());
assert_eq!(flat.unsupported().count(), 1);
assert!(flat.element("nope").is_none());
}
#[test]
fn a_reference_resolves_to_its_declaration() {
let flat = Flat::builder()
.items(
vec![
syn::parse_quote!(
pub type Session = zenoh::Session;
),
syn::parse_quote!(
pub fn session_close(s: Session) {}
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("parses");
let f = flat.function("session_close").expect("declared");
let TypeKind::Named { id, .. } = &f.params[0].ty.kind else {
panic!("a nominal type");
};
let target = flat.resolve(id).expect("resolves");
assert!(matches!(target, Type::Extern(_)));
assert_eq!(target.name(), "Session");
}
#[test]
fn resolution_spans_feeders_and_declaration_order() {
let flat = Flat::builder()
.items(
vec![
syn::parse_quote!(
pub fn session_close(s: Session) {}
),
syn::parse_quote!(
pub type Session = zenoh::Session;
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("a forward reference resolves");
assert!(flat.function("session_close").is_some());
let flat = Flat::builder()
.items(vec![(
syn::parse_quote!(
pub fn session_close(s: Session) {}
),
loc(),
)])
.items(vec![(
syn::parse_quote!(
pub type Session = zenoh::Session;
),
loc(),
)])
.build()
.expect("a cross-feeder reference resolves");
assert!(flat.function("session_close").is_some());
}
#[test]
fn an_undeclared_reference_refuses_the_referencing_item() {
let flat = Flat::builder()
.items(vec![(
syn::parse_quote!(
pub fn session_close(s: Session) {}
),
loc(),
)])
.build()
.expect("a refusal is deferred, not fatal");
assert!(flat.function("session_close").is_none());
let u = flat.unsupported().next().expect("one refusal");
assert!(matches!(
&*u.error,
ItemError::UnresolvedType { name } if name == "Session"
));
assert_eq!(u.name.as_ref().expect("named"), "session_close");
for ty in [
quote::quote!(Option<Session>),
quote::quote!(Vec<Session>),
quote::quote!(&Session),
quote::quote!(Result<Session, Error>),
quote::quote!(impl Fn(Session) + Send + Sync + 'static),
quote::quote!([Session; 4]),
] {
let flat = Flat::builder()
.items(vec![
(opaque("Error"), loc()),
(
syn::parse_quote!(
pub fn f(s: #ty) {}
),
loc(),
),
])
.build()
.expect("deferred");
assert_eq!(
flat.unsupported().count(),
1,
"`Session` must be found inside {}",
ty
);
}
}
#[test]
fn an_out_parameter_is_a_mutable_borrow_of_a_slot() {
let out = lower(quote::quote!(&mut MaybeUninit<Sample>)).expect("in the language");
let TypeKind::Ref { mutable, inner, .. } = &out.kind else {
panic!("a borrow");
};
assert!(mutable);
let TypeKind::Uninit(slot) = &inner.kind else {
panic!("the slot the source wrote");
};
let TypeKind::Named { id, .. } = &slot.kind else {
panic!("the value's own type");
};
assert_eq!(id.name, "Sample");
let target = out.borrow_target().expect("a borrow");
assert!(matches!(&target.kind, TypeKind::Named { id, .. } if id.name == "Sample"));
assert!(!out.is_exclusive_borrow());
for (spelling, exclusive) in [
(quote::quote!(&Sample), false),
(quote::quote!(&mut Sample), true),
] {
let ty = lower(spelling).expect("in the language");
assert_eq!(ty.is_exclusive_borrow(), exclusive);
assert!(
matches!(&ty.borrow_target().expect("a borrow").kind, TypeKind::Named { id, .. } if id.name == "Sample")
);
}
assert_eq!(
reason(quote::quote!(MaybeUninit<Sample>)),
UnsupportedTypeReason::OwnedUninit
);
assert_eq!(
reason(quote::quote!(&MaybeUninit<Sample>)),
UnsupportedTypeReason::SharedUninit
);
let flat = Flat::builder()
.items(vec![
(opaque("Sample"), loc()),
(
syn::parse_quote!(
pub fn get(out: &mut MaybeUninit<Sample>) -> bool {}
),
loc(),
),
])
.build()
.expect("parses");
assert!(flat.function("get").is_some());
}
#[test]
fn refusal_is_transitive() {
let broken: syn::Item = syn::parse_quote!(
pub struct Broken {
pub field: Missing,
}
);
let user: syn::Item = syn::parse_quote!(
pub fn use_broken(value: Broken) {}
);
for (label, items) in [
("declaration first", vec![broken.clone(), user.clone()]),
("dependent first", vec![user, broken]),
] {
let flat = Flat::builder()
.items(items.into_iter().map(|i| (i, loc())))
.build()
.expect("deferred, not fatal");
assert!(
flat.declared_type("Broken").is_none(),
"{label}: `Missing` is undeclared"
);
assert!(
flat.function("use_broken").is_none(),
"{label}: `Broken` is no longer a declaration either"
);
assert_eq!(flat.unsupported().count(), 2, "{label}");
assert!(flat.element("Broken").is_some(), "{label}");
assert!(flat.element("use_broken").is_some(), "{label}");
}
}
#[test]
fn refusal_is_transitive_through_a_chain() {
let chain: Vec<syn::Item> = vec![
syn::parse_quote!(
pub struct A {
pub field: Missing,
}
),
syn::parse_quote!(
pub struct B {
pub field: A,
}
),
syn::parse_quote!(
pub struct C {
pub field: B,
}
),
syn::parse_quote!(
pub fn takes_c(value: C) {}
),
];
for (label, items) in [
("forward", chain.clone()),
("reversed", chain.into_iter().rev().collect()),
] {
let flat = Flat::builder()
.items(items.into_iter().map(|i| (i, loc())))
.build()
.expect("deferred");
assert_eq!(
flat.types().count(),
0,
"{label}: the whole chain collapses"
);
assert_eq!(flat.functions().count(), 0, "{label}");
assert_eq!(flat.unsupported().count(), 4, "{label}");
}
let flat = Flat::builder()
.items(
vec![
opaque("Missing"),
syn::parse_quote!(
pub struct A {
pub field: Missing,
}
),
syn::parse_quote!(
pub fn takes_a(value: A) {}
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("parses");
assert_eq!(flat.unsupported().count(), 0);
assert!(flat.function("takes_a").is_some());
}
#[test]
fn every_surviving_reference_resolves() {
let flat = Flat::builder()
.items(
vec![
opaque("Missing"),
syn::parse_quote!(
pub struct Held {
pub field: Missing,
}
),
syn::parse_quote!(
pub fn takes_held(value: Held) -> Held {}
),
syn::parse_quote!(
pub struct Broken {
pub field: Absent,
}
),
syn::parse_quote!(
pub fn takes_broken(value: Broken) {}
),
]
.into_iter()
.map(|i: syn::Item| (i, loc())),
)
.build()
.expect("deferred");
for f in flat.functions() {
for r in f.params.iter().map(|p| &p.ty).chain([&f.ret]) {
if let TypeKind::Named { id, .. } = &r.kind {
assert!(
flat.resolve(id).is_some(),
"`{}` must resolve from a surviving function",
id.name
);
}
}
}
assert!(flat.function("takes_held").is_some());
assert!(flat.function("takes_broken").is_none());
}
#[test]
fn a_generic_alias_is_refused() {
for (item, param, kind_str) in [
(
syn::parse_quote!(
pub type Handle<T> = hidden::Handle<T>;
),
"T",
"a type parameter",
),
(
syn::parse_quote!(
pub type Padded<const N: usize> = hidden::Padded<N>;
),
"N",
"a const generic parameter",
),
] {
let element = parse_one(item);
let ItemError::UnsupportedGenericParam { param: got, kind } = as_unsupported(&element)
else {
panic!(
"expected a generic-parameter diagnosis, got {}",
describe(&element)
);
};
assert_eq!(got, param);
assert_eq!(*kind, kind_str);
}
let element = parse_one(syn::parse_quote!(
pub type Borrowed<'a> = hidden::Borrowed<'a>;
));
assert_eq!(as_extern(&element).name, "Borrowed");
}
#[test]
fn the_feeders_accumulate_and_whole_stream_rules_span_them() {
let marker: syn::Item = syn::parse_quote!(
pub struct Marker {
pub tag: [u8; TAG_LEN],
}
);
let flat = Flat::builder()
.items(vec![(marker.clone(), loc())])
.items(vec![(tag_len_const(), loc())])
.build()
.expect("the const is found across feeders");
let elements: Vec<Element> = flat.elements().cloned().collect();
assert_eq!(elements.len(), 2);
assert_eq!(
as_struct(&elements[0]).fields[0]
.ty
.array_extent()
.expect("an extent")
.value,
4
);
let err = Flat::builder()
.items(vec![(marker.clone(), loc())])
.items(vec![(marker, loc())])
.build()
.expect_err("a duplicate across feeders is still a duplicate");
let ParseError::DuplicateName(d) = err;
assert_eq!(d.name, "Marker");
}
#[test]
fn duplicate_names_are_a_hard_error() {
let err = try_parse(vec![
syn::parse_quote!(
pub struct Sample {
pub x: u8,
}
),
syn::parse_quote!(
pub fn Sample() {}
),
])
.expect_err("a duplicate");
let ParseError::DuplicateName(d) = err;
assert_eq!(d.name, "Sample");
}
#[test]
fn an_unsupported_item_still_holds_its_name() {
assert!(try_parse(vec![
syn::parse_quote!(
pub fn Thing(pair: (u8, u8)) {}
),
syn::parse_quote!(
pub struct Thing {
pub x: u8,
}
),
])
.is_err());
}
#[test]
fn the_layer_stack_stops_at_an_out_of_order_layer() {
use crate::shape::Shape;
let shape_of = |ty: proc_macro2::TokenStream| {
let reading = lower(ty).expect("lowers");
let (shape, core) = reading.layer_stack();
let rendered = match &shape {
Shape::Base => "Base".to_string(),
Shape::Optional(_, i) => match &**i {
Shape::Base => "Optional(Base)".to_string(),
Shape::Iterable(_) => "Optional(Iterable(Base))".to_string(),
Shape::Optional(..) => "Optional(Optional(..))".to_string(),
},
Shape::Iterable(i) => match &**i {
Shape::Base => "Iterable(Base)".to_string(),
other => format!("Iterable({other:?})"),
},
};
(
rendered,
quote::ToTokens::to_token_stream(core.origin.as_syn()).to_string(),
reading.layer_types().len(),
)
};
assert_eq!(
shape_of(quote::quote!(Option<Vec<Sample>>)),
("Optional(Iterable(Base))".into(), "Sample".into(), 3)
);
assert_eq!(
shape_of(quote::quote!(Option<Sample>)),
("Optional(Base)".into(), "Sample".into(), 2)
);
assert_eq!(
shape_of(quote::quote!(Vec<Sample>)),
("Iterable(Base)".into(), "Sample".into(), 2)
);
assert_eq!(
shape_of(quote::quote!(Vec<Option<Sample>>)),
("Iterable(Base)".into(), "Option < Sample >".into(), 2)
);
assert_eq!(
shape_of(quote::quote!(Option<Option<Sample>>)),
("Optional(Base)".into(), "Option < Sample >".into(), 2)
);
assert_eq!(
shape_of(quote::quote!(Option<Vec<&Sample>>)),
("Optional(Iterable(Base))".into(), "& Sample".into(), 3)
);
}
#[test]
fn a_composed_type_keys_as_its_spelling() {
use crate::flat::{ScalarKind, TypeKey, TypeKind, TypeRef};
let t = lower(quote::quote!(u64)).expect("in the language");
let borrowed = t.borrowed();
assert_eq!(borrowed.key(), TypeKey::from_type(&syn::parse_quote!(&u64)));
assert!(matches!(borrowed.kind, TypeKind::Ref { .. }));
assert_eq!(borrowed.borrow_target().expect("a borrow").key(), t.key());
let optional = t.optional();
assert_eq!(
optional.key(),
TypeKey::from_type(&syn::parse_quote!(Option<u64>))
);
assert_eq!(optional.optional_inner().expect("optional").key(), t.key());
assert_eq!(
TypeRef::scalar(ScalarKind::Bool).key(),
TypeKey::from_type(&syn::parse_quote!(bool))
);
assert_eq!(
TypeRef::scalar(ScalarKind::I32).key(),
TypeKey::from_type(&syn::parse_quote!(i32))
);
assert_eq!(
TypeRef::named(&syn::parse_quote!(ZEnum)).key(),
TypeKey::from_type(&syn::parse_quote!(ZEnum))
);
assert!(
!TypeRef::scalar(ScalarKind::Bool)
.origin
.location
.has_position(),
"a scalar no source wrote carries no position"
);
assert_eq!(&*borrowed.origin.location, &*t.origin.location);
}
#[test]
fn a_raw_identifier_survives_typeid() {
use crate::flat::{TypeKind, TypeRef};
let raw: syn::Ident = syn::parse_quote!(r#type);
assert_eq!(raw.to_string(), "r#type", "the hash is part of the name");
let t = TypeRef::named(&raw);
let TypeKind::Named { id, .. } = &t.kind else {
panic!("named")
};
let back = id.ident().expect("a raw ident is still an ident");
assert_eq!(back, raw);
assert_eq!(tokens(t.origin.as_syn()), "r#type");
let qualified = crate::flat::TypeId {
name: "foreign::Option".to_string(),
};
assert!(qualified.ident().is_none());
}