Repository: Ralith/hecs
Pull request #110 (2020-11-08 02:03:59)

derive(Query) support
Monomorphic structs only, but otherwise fairly unrestricted.

Ralith: Had to strip out the expected error for wrong_lifetime.rs due to rustc beta changing the error format and https://github.com/actions/toolkit/issues/399 not letting us gracefully ignore that. Hopefully a short term issue.

Patch:
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
index 2bbf960..80835f5 100644
--- a/macros/Cargo.toml
+++ b/macros/Cargo.toml
@@ -10,6 +10,6 @@ license = "Apache-2.0"
 proc-macro = true
 
 [dependencies]
-syn = { version = "1.0", default-features = false, features = ["proc-macro", "parsing", "printing", "derive"] }
+syn = { version = "1.0", default-features = false, features = ["proc-macro", "parsing", "printing", "derive", "clone-impls", "visit-mut"] }
 quote = "1.0.3"
 proc-macro2 = "1.0.1"
diff --git a/macros/src/bundle.rs b/macros/src/bundle.rs
new file mode 100644
index 0000000..21e2601
--- /dev/null
+++ b/macros/src/bundle.rs
@@ -0,0 +1,198 @@
+use std::borrow::Cow;
+
+use proc_macro2::Span;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::{DeriveInput, Error, Result};
+
+pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
+    let ident = input.ident;
+    let data = match input.data {
+        syn::Data::Struct(s) => s,
+        _ => {
+            return Err(Error::new_spanned(
+                ident,
+                "derive(Bundle) does not support enums or unions",
+            ))
+        }
+    };
+    let (tys, field_members) = struct_fields(&data.fields);
+    let field_idents = member_as_idents(&field_members);
+    let generics = add_additional_bounds_to_generic_params(input.generics);
+
+    let dyn_bundle_code = gen_dynamic_bundle_impl(&ident, &generics, &field_members, &tys);
+    let bundle_code = if tys.is_empty() {
+        gen_unit_struct_bundle_impl(ident, &generics)
+    } else {
+        gen_bundle_impl(&ident, &generics, &field_members, &field_idents, &tys)
+    };
+    let mut ts = dyn_bundle_code;
+    ts.extend(bundle_code);
+    Ok(ts)
+}
+
+fn gen_dynamic_bundle_impl(
+    ident: &syn::Ident,
+    generics: &syn::Generics,
+    field_members: &[syn::Member],
+    tys: &[&syn::Type],
+) -> TokenStream2 {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    quote! {
+        impl #impl_generics ::hecs::DynamicBundle for #ident #ty_generics #where_clause {
+            fn with_ids<__hecs__T>(&self, f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T {
+                <Self as ::hecs::Bundle>::with_static_ids(f)
+            }
+
+            fn type_info(&self) -> ::std::vec::Vec<::hecs::TypeInfo> {
+                <Self as ::hecs::Bundle>::static_type_info()
+            }
+
+            #[allow(clippy::forget_copy)]
+            unsafe fn put(mut self, mut f: impl ::std::ops::FnMut(*mut u8, ::hecs::TypeInfo)) {
+                #(
+                    f((&mut self.#field_members as *mut #tys).cast::<u8>(), ::hecs::TypeInfo::of::<#tys>());
+                    ::std::mem::forget(self.#field_members);
+                )*
+            }
+        }
+    }
+}
+
+fn gen_bundle_impl(
+    ident: &syn::Ident,
+    generics: &syn::Generics,
+    field_members: &[syn::Member],
+    field_idents: &[Cow<syn::Ident>],
+    tys: &[&syn::Type],
+) -> TokenStream2 {
+    let num_tys = tys.len();
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    let with_static_ids_inner = quote! {
+        {
+            let mut tys = [#((::std::mem::align_of::<#tys>(), ::std::any::TypeId::of::<#tys>())),*];
+            tys.sort_unstable_by(|x, y| {
+                ::std::cmp::Ord::cmp(&x.0, &y.0)
+                    .reverse()
+                    .then(::std::cmp::Ord::cmp(&x.1, &y.1))
+            });
+            let mut ids = [::std::any::TypeId::of::<()>(); #num_tys];
+            for (id, info) in ::std::iter::Iterator::zip(ids.iter_mut(), tys.iter()) {
+                *id = info.1;
+            }
+            ids
+        }
+    };
+    let with_static_ids_body = if generics.params.is_empty() {
+        quote! {
+            ::hecs::lazy_static::lazy_static! {
+                static ref ELEMENTS: [::std::any::TypeId; #num_tys] = {
+                    #with_static_ids_inner
+                };
+            }
+            f(&*ELEMENTS)
+        }
+    } else {
+        quote! {
+            f(&#with_static_ids_inner)
+        }
+    };
+    quote! {
+        impl #impl_generics ::hecs::Bundle for #ident #ty_generics #where_clause {
+            #[allow(non_camel_case_types)]
+            fn with_static_ids<__hecs__T>(f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T {
+                #with_static_ids_body
+            }
+
+            fn static_type_info() -> ::std::vec::Vec<::hecs::TypeInfo> {
+                let mut info = ::std::vec![#(::hecs::TypeInfo::of::<#tys>()),*];
+                info.sort_unstable();
+                info
+            }
+
+            unsafe fn get(
+                mut f: impl ::std::ops::FnMut(::hecs::TypeInfo) -> ::std::option::Option<::std::ptr::NonNull<u8>>,
+            ) -> ::std::result::Result<Self, ::hecs::MissingComponent> {
+                #(
+                    let #field_idents = f(::hecs::TypeInfo::of::<#tys>())
+                            .ok_or_else(::hecs::MissingComponent::new::<#tys>)?
+                            .cast::<#tys>()
+                            .as_ptr();
+                )*
+                ::std::result::Result::Ok(Self { #( #field_members: #field_idents.read(), )* })
+            }
+        }
+    }
+}
+
+// no reason to generate a static for unit structs
+fn gen_unit_struct_bundle_impl(ident: syn::Ident, generics: &syn::Generics) -> TokenStream2 {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    quote! {
+        impl #impl_generics ::hecs::Bundle for #ident #ty_generics #where_clause {
+            #[allow(non_camel_case_types)]
+            fn with_static_ids<__hecs__T>(f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T { f(&[]) }
+            fn static_type_info() -> ::std::vec::Vec<::hecs::TypeInfo> { ::std::vec::Vec::new() }
+
+            unsafe fn get(
+                mut f: impl ::std::ops::FnMut(::hecs::TypeInfo) -> ::std::option::Option<::std::ptr::NonNull<u8>>,
+            ) -> ::std::result::Result<Self, ::hecs::MissingComponent> {
+                ::std::result::Result::Ok(Self {/* for some reason this works for all unit struct variations */})
+            }
+        }
+    }
+}
+
+fn make_component_trait_bound() -> syn::TraitBound {
+    syn::TraitBound {
+        paren_token: None,
+        modifier: syn::TraitBoundModifier::None,
+        lifetimes: None,
+        path: syn::parse_quote!(::hecs::Component),
+    }
+}
+
+fn add_additional_bounds_to_generic_params(mut generics: syn::Generics) -> syn::Generics {
+    generics.type_params_mut().for_each(|tp| {
+        tp.bounds
+            .push(syn::TypeParamBound::Trait(make_component_trait_bound()))
+    });
+    generics
+}
+
+fn struct_fields(fields: &syn::Fields) -> (Vec<&syn::Type>, Vec<syn::Member>) {
+    match fields {
+        syn::Fields::Named(ref fields) => fields
+            .named
+            .iter()
+            .map(|f| (&f.ty, syn::Member::Named(f.ident.clone().unwrap())))
+            .unzip(),
+        syn::Fields::Unnamed(ref fields) => fields
+            .unnamed
+            .iter()
+            .enumerate()
+            .map(|(i, f)| {
+                (
+                    &f.ty,
+                    syn::Member::Unnamed(syn::Index {
+                        index: i as u32,
+                        span: Span::call_site(),
+                    }),
+                )
+            })
+            .unzip(),
+        syn::Fields::Unit => (Vec::new(), Vec::new()),
+    }
+}
+
+fn member_as_idents<'a>(members: &'a [syn::Member]) -> Vec<Cow<'a, syn::Ident>> {
+    members
+        .iter()
+        .map(|member| match member {
+            syn::Member::Named(ident) => Cow::Borrowed(ident),
+            &syn::Member::Unnamed(syn::Index { index, span }) => {
+                Cow::Owned(syn::Ident::new(&format!("tuple_field_{}", index), span))
+            }
+        })
+        .collect()
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 483aa1f..a17dca4 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -14,219 +14,39 @@
 
 extern crate proc_macro;
 
-use std::borrow::Cow;
+mod bundle;
+mod query;
 
 use proc_macro::TokenStream;
-use proc_macro2::Span;
-use proc_macro2::TokenStream as TokenStream2;
-use quote::quote;
-use syn::{parse_macro_input, DeriveInput, Error, Result};
+use syn::{parse_macro_input, DeriveInput};
 
-/// Implement `Bundle` for a monomorphic struct
+/// Implement `Bundle` for a struct
 ///
 /// Bundles can be passed directly to `World::spawn` and `World::insert`, and obtained from
 /// `World::remove`. Monomorphic `Bundle` implementations are slightly more efficient than the
 /// polymorphic implementations for tuples, and can be convenient when combined with other derives
 /// like `serde::Deserialize`.
-#[allow(clippy::cognitive_complexity)]
 #[proc_macro_derive(Bundle)]
 pub fn derive_bundle(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as DeriveInput);
-    match derive_bundle_(input) {
+    match bundle::derive(input) {
         Ok(ts) => ts,
         Err(e) => e.to_compile_error(),
     }
     .into()
 }
 
-fn derive_bundle_(input: DeriveInput) -> Result<TokenStream2> {
-    let ident = input.ident;
-    let data = match input.data {
-        syn::Data::Struct(s) => s,
-        _ => {
-            return Err(Error::new_spanned(
-                ident,
-                "derive(Bundle) does not support enums or unions",
-            ))
-        }
-    };
-    let (tys, field_members) = struct_fields(&data.fields);
-    let field_idents = member_as_idents(&field_members);
-    let generics = add_additional_bounds_to_generic_params(input.generics);
-
-    let dyn_bundle_code = gen_dynamic_bundle_impl(&ident, &generics, &field_members, &tys);
-    let bundle_code = if tys.is_empty() {
-        gen_unit_struct_bundle_impl(ident, &generics)
-    } else {
-        gen_bundle_impl(&ident, &generics, &field_members, &field_idents, &tys)
-    };
-    let mut ts = dyn_bundle_code;
-    ts.extend(bundle_code);
-    Ok(ts)
-}
-
-fn gen_dynamic_bundle_impl(
-    ident: &syn::Ident,
-    generics: &syn::Generics,
-    field_members: &[syn::Member],
-    tys: &[&syn::Type],
-) -> TokenStream2 {
-    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-    quote! {
-        impl #impl_generics ::hecs::DynamicBundle for #ident #ty_generics #where_clause {
-            fn with_ids<__hecs__T>(&self, f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T {
-                <Self as ::hecs::Bundle>::with_static_ids(f)
-            }
-
-            fn type_info(&self) -> ::std::vec::Vec<::hecs::TypeInfo> {
-                <Self as ::hecs::Bundle>::static_type_info()
-            }
-
-            #[allow(clippy::forget_copy)]
-            unsafe fn put(mut self, mut f: impl ::std::ops::FnMut(*mut u8, ::hecs::TypeInfo)) {
-                #(
-                    f((&mut self.#field_members as *mut #tys).cast::<u8>(), ::hecs::TypeInfo::of::<#tys>());
-                    ::std::mem::forget(self.#field_members);
-                )*
-            }
-        }
-    }
-}
-
-fn gen_bundle_impl(
-    ident: &syn::Ident,
-    generics: &syn::Generics,
-    field_members: &[syn::Member],
-    field_idents: &[Cow<syn::Ident>],
-    tys: &[&syn::Type],
-) -> TokenStream2 {
-    let num_tys = tys.len();
-    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-    let with_static_ids_inner = quote! {
-        {
-            let mut tys = [#((::std::mem::align_of::<#tys>(), ::std::any::TypeId::of::<#tys>())),*];
-            tys.sort_unstable_by(|x, y| {
-                ::std::cmp::Ord::cmp(&x.0, &y.0)
-                    .reverse()
-                    .then(::std::cmp::Ord::cmp(&x.1, &y.1))
-            });
-            let mut ids = [::std::any::TypeId::of::<()>(); #num_tys];
-            for (id, info) in ::std::iter::Iterator::zip(ids.iter_mut(), tys.iter()) {
-                *id = info.1;
-            }
-            ids
-        }
-    };
-    let with_static_ids_body = if generics.params.is_empty() {
-        quote! {
-            ::hecs::lazy_static::lazy_static! {
-                static ref ELEMENTS: [::std::any::TypeId; #num_tys] = {
-                    #with_static_ids_inner
-                };
-            }
-            f(&*ELEMENTS)
-        }
-    } else {
-        quote! {
-            f(&#with_static_ids_inner)
-        }
-    };
-    quote! {
-        impl #impl_generics ::hecs::Bundle for #ident #ty_generics #where_clause {
-            #[allow(non_camel_case_types)]
-            fn with_static_ids<__hecs__T>(f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T {
-                #with_static_ids_body
-            }
-
-            fn static_type_info() -> ::std::vec::Vec<::hecs::TypeInfo> {
-                let mut info = ::std::vec![#(::hecs::TypeInfo::of::<#tys>()),*];
-                info.sort_unstable();
-                info
-            }
-
-            unsafe fn get(
-                mut f: impl ::std::ops::FnMut(::hecs::TypeInfo) -> ::std::option::Option<::std::ptr::NonNull<u8>>,
-            ) -> ::std::result::Result<Self, ::hecs::MissingComponent> {
-                #(
-                    let #field_idents = f(::hecs::TypeInfo::of::<#tys>())
-                            .ok_or_else(::hecs::MissingComponent::new::<#tys>)?
-                            .cast::<#tys>()
-                            .as_ptr();
-                )*
-                ::std::result::Result::Ok(Self { #( #field_members: #field_idents.read(), )* })
-            }
-        }
-    }
-}
-
-// no reason to generate a static for unit structs
-fn gen_unit_struct_bundle_impl(ident: syn::Ident, generics: &syn::Generics) -> TokenStream2 {
-    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-    quote! {
-        impl #impl_generics ::hecs::Bundle for #ident #ty_generics #where_clause {
-            #[allow(non_camel_case_types)]
-            fn with_static_ids<__hecs__T>(f: impl ::std::ops::FnOnce(&[::std::any::TypeId]) -> __hecs__T) -> __hecs__T { f(&[]) }
-            fn static_type_info() -> ::std::vec::Vec<::hecs::TypeInfo> { ::std::vec::Vec::new() }
-
-            unsafe fn get(
-                mut f: impl ::std::ops::FnMut(::hecs::TypeInfo) -> Option<::std::ptr::NonNull<u8>>,
-            ) -> Result<Self, ::hecs::MissingComponent> {
-                Ok(Self {/* for some reason this works for all unit struct variations */})
-            }
-        }
-    }
-}
-
-fn make_component_trait_bound() -> syn::TraitBound {
-    syn::TraitBound {
-        paren_token: None,
-        modifier: syn::TraitBoundModifier::None,
-        lifetimes: None,
-        path: syn::parse_quote!(::hecs::Component),
-    }
-}
-
-fn add_additional_bounds_to_generic_params(mut generics: syn::Generics) -> syn::Generics {
-    generics.type_params_mut().for_each(|tp| {
-        tp.bounds
-            .push(syn::TypeParamBound::Trait(make_component_trait_bound()))
-    });
-    generics
-}
-
-fn struct_fields(fields: &syn::Fields) -> (Vec<&syn::Type>, Vec<syn::Member>) {
-    match fields {
-        syn::Fields::Named(ref fields) => fields
-            .named
-            .iter()
-            .map(|f| (&f.ty, syn::Member::Named(f.ident.clone().unwrap())))
-            .unzip(),
-        syn::Fields::Unnamed(ref fields) => fields
-            .unnamed
-            .iter()
-            .enumerate()
-            .map(|(i, f)| {
-                (
-                    &f.ty,
-                    syn::Member::Unnamed(syn::Index {
-                        index: i as u32,
-                        span: Span::call_site(),
-                    }),
-                )
-            })
-            .unzip(),
-        syn::Fields::Unit => (Vec::new(), Vec::new()),
+/// Implement `Query` for a struct
+///
+/// Queries structs can be passed to the type parameter of `World::query`. They must have exactly
+/// one lifetime parameter, and all of their fields must be queries (e.g. references) using that
+/// lifetime.
+#[proc_macro_derive(Query)]
+pub fn derive_query(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    match query::derive(input) {
+        Ok(ts) => ts,
+        Err(e) => e.to_compile_error(),
     }
-}
-
-fn member_as_idents<'a>(members: &'a [syn::Member]) -> Vec<Cow<'a, syn::Ident>> {
-    members
-        .iter()
-        .map(|member| match member {
-            syn::Member::Named(ident) => Cow::Borrowed(ident),
-            &syn::Member::Unnamed(syn::Index { index, span }) => {
-                Cow::Owned(syn::Ident::new(&format!("tuple_field_{}", index), span))
-            }
-        })
-        .collect()
+    .into()
 }
diff --git a/macros/src/query.rs b/macros/src/query.rs
new file mode 100644
index 0000000..1dd861b
--- /dev/null
+++ b/macros/src/query.rs
@@ -0,0 +1,160 @@
+use proc_macro2::Span;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::{DeriveInput, Error, Ident, Lifetime, Result, Type};
+
+pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
+    let ident = input.ident;
+    let vis = input.vis;
+    let data = match input.data {
+        syn::Data::Struct(s) => s,
+        _ => {
+            return Err(Error::new_spanned(
+                ident,
+                "derive(Query) may only be applied to structs",
+            ))
+        }
+    };
+    let lifetime = input
+        .generics
+        .lifetimes()
+        .next()
+        .map(|x| x.lifetime.clone());
+    let lifetime = match lifetime {
+        Some(x) => x,
+        None => {
+            return Err(Error::new_spanned(
+                input.generics,
+                "must have exactly one lifetime parameter",
+            ))
+        }
+    };
+    if input.generics.params.len() != 1 {
+        return Err(Error::new_spanned(
+            ident,
+            "must have exactly one lifetime parameter and no type parameters",
+        ));
+    }
+
+    let (fields, fetches) = match data.fields {
+        syn::Fields::Named(ref fields) => fields
+            .named
+            .iter()
+            .map(|f| {
+                (
+                    syn::Member::Named(f.ident.clone().unwrap()),
+                    query_fetch_ty(&lifetime, &f.ty),
+                )
+            })
+            .unzip(),
+        syn::Fields::Unnamed(ref fields) => fields
+            .unnamed
+            .iter()
+            .enumerate()
+            .map(|(i, f)| {
+                (
+                    syn::Member::Unnamed(syn::Index {
+                        index: i as u32,
+                        span: Span::call_site(),
+                    }),
+                    query_fetch_ty(&lifetime, &f.ty),
+                )
+            })
+            .unzip(),
+        syn::Fields::Unit => (Vec::new(), Vec::new()),
+    };
+    let fetches = fetches.into_iter().collect::<Result<Vec<_>>>()?;
+    let fetch_ident = Ident::new(&format!("__HecsInternal{}Fetch", ident), Span::call_site());
+    let fetch = match data.fields {
+        syn::Fields::Named(_) => quote! {
+            #vis struct #fetch_ident {
+                #(
+                    #fields: #fetches,
+                )*
+            }
+        },
+        syn::Fields::Unnamed(_) => quote! {
+            #vis struct #fetch_ident(#(#fetches),*);
+        },
+        syn::Fields::Unit => quote! {
+            #vis struct #fetch_ident;
+        },
+    };
+
+    Ok(quote! {
+        impl<'a> ::hecs::Query for #ident<'a> {
+            type Fetch = #fetch_ident;
+        }
+
+        #[doc(hidden)]
+        #fetch
+
+        impl<'a> ::hecs::Fetch<'a> for #fetch_ident {
+            type Item = #ident<'a>;
+
+            fn dangling() -> Self {
+                Self {
+                    #(
+                        #fields: #fetches::dangling(),
+                    )*
+                }
+            }
+
+            #[allow(unused_variables, unused_mut)]
+            fn access(archetype: &::hecs::Archetype) -> ::std::option::Option<::hecs::Access> {
+                let mut access = ::hecs::Access::Iterate;
+                #(
+                    access = ::core::cmp::max(access, #fetches::access(archetype)?);
+                )*
+                ::std::option::Option::Some(access)
+            }
+
+            #[allow(unused_variables)]
+            fn borrow(archetype: &::hecs::Archetype) {
+                #(#fetches::borrow(archetype);)*
+            }
+
+            #[allow(unused_variables)]
+            fn new(archetype: &'a ::hecs::Archetype) -> ::std::option::Option<Self> {
+                ::std::option::Option::Some(Self {
+                    #(
+                        #fields: #fetches::new(archetype)?,
+                    )*
+                })
+            }
+
+            #[allow(unused_variables)]
+            fn release(archetype: &::hecs::Archetype) {
+                #(#fetches::release(archetype);)*
+            }
+
+            #[allow(unused_variables)]
+            unsafe fn get(&self, n: usize) -> Self::Item {
+                #ident {
+                    #(
+                        #fields: <#fetches as ::hecs::Fetch<'a>>::get(&self.#fields, n),
+                    )*
+                }
+            }
+        }
+    })
+}
+
+fn query_fetch_ty(lifetime: &Lifetime, ty: &Type) -> Result<TokenStream2> {
+    struct Visitor<'a> {
+        replace: &'a Lifetime,
+    };
+    impl syn::visit_mut::VisitMut for Visitor<'_> {
+        fn visit_lifetime_mut(&mut self, l: &mut Lifetime) {
+            if l == self.replace {
+                *l = Lifetime::new("'static", Span::call_site());
+            }
+        }
+    }
+
+    let mut ty = ty.clone();
+    syn::visit_mut::visit_type_mut(&mut Visitor { replace: lifetime }, &mut ty);
+    Ok(quote! {
+        <#ty as ::hecs::Query>::Fetch
+    })
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8fcb241..92515b1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -90,4 +90,4 @@ pub use lazy_static;
 pub use query::Fetch;
 
 #[cfg(feature = "macros")]
-pub use hecs_macros::Bundle;
+pub use hecs_macros::{Bundle, Query};


Test patch:
diff --git a/tests/derive.rs b/tests/derive.rs
index ca570ce..8942943 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -6,13 +6,15 @@
 fn derive() {
     const TEST_DIR: &str = "tests/derive";
     let t = trybuild::TestCases::new();
-    let failures = &["enum.rs", "union.rs"];
+    let failures = &["enum.rs", "union.rs", "wrong_lifetime.rs"];
     let successes = &[
         "unit_structs.rs",
         "tuple_structs.rs",
         "named_structs.rs",
         "no_prelude.rs",
         "generics.rs",
+        "nested_query.rs",
+        "export.rs",
     ];
     for &passing_test in successes {
         t.pass(&format!("{}/{}", TEST_DIR, passing_test));
diff --git a/tests/derive/enum.rs b/tests/derive/enum.rs
index 569f1d7..765f95a 100644
--- a/tests/derive/enum.rs
+++ b/tests/derive/enum.rs
@@ -1,6 +1,6 @@
-use hecs::Bundle;
+use hecs::{Bundle, Query};
 
-#[derive(Bundle)]
+#[derive(Bundle, Query)]
 enum Foo {}
 
 fn main() {}
diff --git a/tests/derive/enum.stderr b/tests/derive/enum.stderr
index bec72c6..053ad02 100644
--- a/tests/derive/enum.stderr
+++ b/tests/derive/enum.stderr
@@ -1,3 +1,9 @@
+error: derive(Query) may only be applied to structs
+ --> $DIR/enum.rs:4:6
+  |
+4 | enum Foo {}
+  |      ^^^
+
 error: derive(Bundle) does not support enums or unions
  --> $DIR/enum.rs:4:6
   |
diff --git a/tests/derive/export.rs b/tests/derive/export.rs
new file mode 100644
index 0000000..115f62f
--- /dev/null
+++ b/tests/derive/export.rs
@@ -0,0 +1,16 @@
+mod inner {
+    use hecs::{Bundle, Query};
+
+    #[derive(Bundle)]
+    pub struct Foo;
+
+    #[derive(Query)]
+    pub struct Bar<'a> {
+        foo: &'a i32,
+    }
+}
+
+type Foo = inner::Foo;
+type Bar<'a> = inner::Bar<'a>;
+
+fn main() {}
diff --git a/tests/derive/named_structs.rs b/tests/derive/named_structs.rs
index 9ef9b26..2b4571a 100644
--- a/tests/derive/named_structs.rs
+++ b/tests/derive/named_structs.rs
@@ -1,4 +1,4 @@
-use hecs::Bundle;
+use hecs::{Bundle, Query};
 
 #[derive(Bundle)]
 struct Foo {
@@ -18,4 +18,10 @@ struct Baz {
     baz: &'static str,
 }
 
+#[derive(Query)]
+struct Quux<'a> {
+    foo: &'a i32,
+    bar: &'a mut bool,
+}
+
 fn main() {}
diff --git a/tests/derive/nested_query.rs b/tests/derive/nested_query.rs
new file mode 100644
index 0000000..9b4f4e8
--- /dev/null
+++ b/tests/derive/nested_query.rs
@@ -0,0 +1,14 @@
+use hecs::Query;
+
+#[derive(Query)]
+struct Foo<'a> {
+    foo: &'a i32,
+    bar: Bar<'a>,
+}
+
+#[derive(Query)]
+struct Bar<'a> {
+    baz: &'a mut bool,
+}
+
+fn main() {}
diff --git a/tests/derive/no_prelude.rs b/tests/derive/no_prelude.rs
index 6d6fd38..acd672c 100644
--- a/tests/derive/no_prelude.rs
+++ b/tests/derive/no_prelude.rs
@@ -5,4 +5,17 @@ struct Foo {
     foo: (),
 }
 
+#[derive(::hecs::Bundle)]
+struct Bar<T> {
+    foo: T,
+}
+
+#[derive(::hecs::Bundle)]
+struct Baz;
+
+#[derive(::hecs::Query)]
+struct Quux<'a> {
+    foo: &'a (),
+}
+
 fn main() {}
diff --git a/tests/derive/tuple_structs.rs b/tests/derive/tuple_structs.rs
index 68729b1..d030c82 100644
--- a/tests/derive/tuple_structs.rs
+++ b/tests/derive/tuple_structs.rs
@@ -1,4 +1,4 @@
-use hecs::Bundle;
+use hecs::{Bundle, Query};
 
 #[derive(Bundle)]
 struct Foo(i32);
@@ -9,4 +9,7 @@ struct Bar(i32, String);
 #[derive(Bundle)]
 struct Baz(i32, String, &'static str);
 
+#[derive(Query)]
+struct Quux<'a>(&'a i32);
+
 fn main() {}
diff --git a/tests/derive/union.rs b/tests/derive/union.rs
index 3c2f1d5..559d654 100644
--- a/tests/derive/union.rs
+++ b/tests/derive/union.rs
@@ -1,6 +1,6 @@
-use hecs::Bundle;
+use hecs::{Bundle, Query};
 
-#[derive(Bundle)]
+#[derive(Bundle, Query)]
 union Foo {
     u8: u8,
 }
diff --git a/tests/derive/union.stderr b/tests/derive/union.stderr
index 87ba7b0..825f650 100644
--- a/tests/derive/union.stderr
+++ b/tests/derive/union.stderr
@@ -1,3 +1,9 @@
+error: derive(Query) may only be applied to structs
+ --> $DIR/union.rs:4:7
+  |
+4 | union Foo {
+  |       ^^^
+
 error: derive(Bundle) does not support enums or unions
  --> $DIR/union.rs:4:7
   |
diff --git a/tests/derive/wrong_lifetime.rs b/tests/derive/wrong_lifetime.rs
new file mode 100644
index 0000000..6c80673
--- /dev/null
+++ b/tests/derive/wrong_lifetime.rs
@@ -0,0 +1,9 @@
+use hecs::Query;
+
+#[derive(Query)]
+struct Foo<'a> {
+    foo: &'a i32,
+    bar: &'static mut bool,
+}
+
+fn main() {}
diff --git a/tests/tests.rs b/tests/tests.rs
index b94f1d6..14477a6 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -62,6 +62,25 @@ fn query_all() {
     assert!(ents.contains(&(f, ())));
 }
 
+#[test]
+#[cfg(feature = "macros")]
+fn derived_query() {
+    #[derive(Query)]
+    struct Foo<'a> {
+        x: &'a i32,
+        y: &'a mut bool,
+    }
+
+    let mut world = World::new();
+    let e = world.spawn((42, false));
+    let ents = world
+        .query::<Foo>()
+        .iter()
+        .map(|(e, q)| (e, *q.x, *q.y))
+        .collect::<Vec<_>>();
+    assert_eq!(ents, &[(e, 42, false)]);
+}
+
 #[test]
 fn query_single_component() {
     let mut world = World::new();