bevy_asset_macros/
lib.rs

1#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
2#![cfg_attr(docsrs, feature(doc_auto_cfg))]
3
4use bevy_macro_utils::BevyManifest;
5use proc_macro::{Span, TokenStream};
6use quote::{format_ident, quote};
7use syn::{parse_macro_input, Data, DeriveInput, Path};
8
9pub(crate) fn bevy_asset_path() -> Path {
10    BevyManifest::shared().get_path("bevy_asset")
11}
12
13const DEPENDENCY_ATTRIBUTE: &str = "dependency";
14
15#[proc_macro_derive(Asset, attributes(dependency))]
16pub fn derive_asset(input: TokenStream) -> TokenStream {
17    let ast = parse_macro_input!(input as DeriveInput);
18    let bevy_asset_path: Path = bevy_asset_path();
19
20    let struct_name = &ast.ident;
21    let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
22    let dependency_visitor = match derive_dependency_visitor_internal(&ast, &bevy_asset_path) {
23        Ok(dependency_visitor) => dependency_visitor,
24        Err(err) => return err.into_compile_error().into(),
25    };
26
27    TokenStream::from(quote! {
28        impl #impl_generics #bevy_asset_path::Asset for #struct_name #type_generics #where_clause { }
29        #dependency_visitor
30    })
31}
32
33#[proc_macro_derive(VisitAssetDependencies, attributes(dependency))]
34pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream {
35    let ast = parse_macro_input!(input as DeriveInput);
36    let bevy_asset_path: Path = bevy_asset_path();
37    match derive_dependency_visitor_internal(&ast, &bevy_asset_path) {
38        Ok(dependency_visitor) => TokenStream::from(dependency_visitor),
39        Err(err) => err.into_compile_error().into(),
40    }
41}
42
43fn derive_dependency_visitor_internal(
44    ast: &DeriveInput,
45    bevy_asset_path: &Path,
46) -> Result<proc_macro2::TokenStream, syn::Error> {
47    let struct_name = &ast.ident;
48    let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
49
50    let visit_dep = |to_read| quote!(#bevy_asset_path::VisitAssetDependencies::visit_dependencies(#to_read, visit););
51    let is_dep_attribute = |a: &syn::Attribute| a.path().is_ident(DEPENDENCY_ATTRIBUTE);
52    let field_has_dep = |f: &syn::Field| f.attrs.iter().any(is_dep_attribute);
53
54    let body = match &ast.data {
55        Data::Struct(data_struct) => {
56            let fields = data_struct.fields.iter();
57            let field_visitors = fields.enumerate().filter(|(_, f)| field_has_dep(f));
58            let field_visitors = field_visitors.map(|(i, field)| match &field.ident {
59                Some(ident) => visit_dep(quote!(&self.#ident)),
60                None => {
61                    let index = syn::Index::from(i);
62                    visit_dep(quote!(&self.#index))
63                }
64            });
65            Some(quote!( #(#field_visitors)* ))
66        }
67        Data::Enum(data_enum) => {
68            let variant_has_dep = |v: &syn::Variant| v.fields.iter().any(field_has_dep);
69            let any_case_required = data_enum.variants.iter().any(variant_has_dep);
70            let cases = data_enum.variants.iter().filter(|v| variant_has_dep(v));
71            let cases = cases.map(|variant| {
72                let ident = &variant.ident;
73                let fields = &variant.fields;
74
75                let field_visitors = fields.iter().enumerate().filter(|(_, f)| field_has_dep(f));
76
77                let field_visitors = field_visitors.map(|(i, field)| match &field.ident {
78                    Some(ident) => visit_dep(quote!(#ident)),
79                    None => {
80                        let ident = format_ident!("member{i}");
81                        visit_dep(quote!(#ident))
82                    }
83                });
84                let fields = match fields {
85                    syn::Fields::Named(fields) => {
86                        let named = fields.named.iter().map(|f| f.ident.as_ref());
87                        quote!({ #(#named,)* .. })
88                    }
89                    syn::Fields::Unnamed(fields) => {
90                        let named = (0..fields.unnamed.len()).map(|i| format_ident!("member{i}"));
91                        quote!( ( #(#named,)* ) )
92                    }
93                    syn::Fields::Unit => unreachable!("Can't pass filter is_dep_attribute"),
94                };
95                quote!(Self::#ident #fields => {
96                    #(#field_visitors)*
97                })
98            });
99
100            any_case_required.then(|| quote!(match self { #(#cases)*, _ => {} }))
101        }
102        Data::Union(_) => {
103            return Err(syn::Error::new(
104                Span::call_site().into(),
105                "Asset derive currently doesn't work on unions",
106            ));
107        }
108    };
109
110    // prevent unused variable warning in case there are no dependencies
111    let visit = if body.is_none() {
112        quote! { _visit }
113    } else {
114        quote! { visit }
115    };
116
117    Ok(quote! {
118        impl #impl_generics #bevy_asset_path::VisitAssetDependencies for #struct_name #type_generics #where_clause {
119            fn visit_dependencies(&self, #visit: &mut impl FnMut(#bevy_asset_path::UntypedAssetId)) {
120                #body
121            }
122        }
123    })
124}