Skip to main content

wowlab_engine_macros_impl/
parse.rs

1use quote::ToTokens;
2use syn::{DeriveInput, Ident, ItemStruct, LitStr, Meta, Path, Token, token::Comma};
3
4// #t(fn: rust_alloc_in_loop) diagnostics allocate only while rejecting invalid macro input.
5pub(crate) fn parse_named_field_attr(
6    input: &DeriveInput,
7    attr_name: &str,
8) -> syn::Result<Option<Ident>> {
9    let mut found: Option<Ident> = None;
10
11    for attr in &input.attrs {
12        if !attr.path().is_ident(attr_name) {
13            continue;
14        }
15
16        if found.is_some() {
17            return Err(syn::Error::new_spanned(
18                attr,
19                format!("duplicate #[{attr_name}] attribute"),
20            ));
21        }
22
23        let name_value = attr.meta.require_name_value().map_err(|error| {
24            syn::Error::new_spanned(
25                &attr.meta,
26                format!("#[{attr_name} = \"...\"] must use `name = \"value\"` form: {error}"),
27            )
28        })?;
29        let lit: LitStr = syn::parse2(name_value.value.to_token_stream()).map_err(|error| {
30            syn::Error::new_spanned(
31                &name_value.value,
32                format!("#[{attr_name}] expects a string literal: {error}"),
33            )
34        })?;
35        let raw = lit.value();
36        let mut parsed: Ident = syn::parse_str(&raw).map_err(|error| {
37            syn::Error::new_spanned(
38                &lit,
39                format!("#[{attr_name}] expects a valid Rust identifier, got `{raw}`: {error}"),
40            )
41        })?;
42        // Re-point at the literal so downstream errors underline the attribute, not call-site noise.
43
44        parsed.set_span(lit.span());
45        found = Some(parsed);
46    }
47
48    Ok(found)
49}
50
51// #t(fn: rust_alloc_in_loop) diagnostics allocate only while rejecting invalid macro input.
52pub(crate) fn parse_trait_crate_attr(
53    input: &DeriveInput,
54    attr_name: &str,
55) -> syn::Result<Option<Path>> {
56    let mut found: Option<Path> = None;
57
58    for attr in &input.attrs {
59        if !attr.path().is_ident(attr_name) {
60            continue;
61        }
62
63        if found.is_some() {
64            return Err(syn::Error::new_spanned(
65                attr,
66                format!("duplicate #[{attr_name}(...)] attribute"),
67            ));
68        }
69
70        let mut path: Option<Path> = None;
71
72        attr.parse_nested_meta(|meta| {
73            attr_keys!(
74                meta,
75                unknown = format!(
76                    "unknown `{attr_name}` attribute, expected `crate = \"...\"`"
77                );
78                "crate" => (
79                    path.is_some(),
80                    format!("duplicate `{attr_name}` crate path")
81                ) {
82                let value = meta.value()?;
83                let lit: LitStr = value.parse()?;
84                let raw = lit.value();
85                let parsed: Path = syn::parse_str(&raw).map_err(|error| {
86                    syn::Error::new_spanned(
87                        &lit,
88                        format!(
89                            "#[{attr_name}(crate = \"...\")] expects a valid Rust path, got `{raw}`: {error}"
90                        ),
91                    )
92                })?;
93
94                path = Some(parsed);
95                }
96            )
97        })?;
98        found = Some(path.ok_or_else(|| {
99            syn::Error::new_spanned(
100                attr,
101                format!("#[{attr_name}(...)] missing required `crate = \"...\"`"),
102            )
103        })?);
104    }
105
106    Ok(found)
107}
108
109pub(crate) struct DefineSlotInput {
110    pub(crate) item: ItemStruct,
111    pub(crate) impl_body: Option<proc_macro2::TokenStream>,
112}
113
114impl syn::parse::Parse for DefineSlotInput {
115    // #t(fn: rust_alloc_in_loop) diagnostics allocate only while rejecting invalid macro input.
116    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
117        let item: ItemStruct = input.parse()?;
118        let mut impl_body = None;
119
120        while !input.is_empty() {
121            if input.peek(Token![impl]) {
122                let impl_token = input.parse::<Token![impl]>()?;
123
124                if impl_body.is_some() {
125                    return Err(syn::Error::new_spanned(
126                        impl_token,
127                        "duplicate `impl` section in define_slot!",
128                    ));
129                }
130
131                let content;
132
133                syn::braced!(content in input);
134                impl_body = Some(content.parse()?);
135            } else {
136                let ident: Ident = input.parse()?;
137
138                return Err(syn::Error::new_spanned(
139                    &ident,
140                    format!("unexpected section `{ident}` in define_slot!, expected `impl`"),
141                ));
142            }
143        }
144
145        Ok(DefineSlotInput { item, impl_body })
146    }
147}
148
149pub(crate) struct SlotAttr {
150    pub(crate) domain: LitStr,
151    pub(crate) kind_ident: Ident,
152    pub(crate) key_domain: Option<LitStr>,
153}
154
155pub(crate) struct ExprAttr {
156    pub(crate) name: LitStr,
157    pub(crate) field_type_ident: Ident,
158    pub(crate) eval_kind_ident: Ident,
159    pub(crate) field_ident: Ident,
160}
161
162fn slot_kind_ident_from_shorthand(ident: &Ident) -> syn::Result<Ident> {
163    match ident.to_string().as_str() {
164        "singleton" => Ok(Ident::new("Singleton", ident.span())),
165        "keyed" => Ok(Ident::new("Keyed", ident.span())),
166        other => Err(syn::Error::new_spanned(
167            ident,
168            format!("unknown slot kind `{other}`, expected `singleton` or `keyed`"),
169        )),
170    }
171}
172
173pub(crate) fn parse_slot_attr(attr: &syn::Attribute) -> syn::Result<SlotAttr> {
174    let mut domain: Option<LitStr> = None;
175    let mut kind_ident: Option<Ident> = None;
176    let mut key_domain: Option<LitStr> = None;
177
178    attr.parse_nested_meta(|meta| {
179        attr_keys!(
180            meta,
181            unknown = "unknown slot attribute, expected `domain`, `kind`, or `key_domain`";
182            "domain" => (domain.is_some(), "duplicate `domain` slot attribute") {
183            domain = Some(meta.value()?.parse()?);
184            },
185            "kind" => (kind_ident.is_some(), "duplicate `kind` slot attribute") {
186            let ident: Ident = meta.value()?.parse()?;
187
188            kind_ident = Some(slot_kind_ident_from_shorthand(&ident)?);
189            },
190            "key_domain" => (
191                key_domain.is_some(),
192                "duplicate `key_domain` slot attribute"
193            ) {
194            key_domain = Some(meta.value()?.parse()?);
195            }
196        )
197    })?;
198
199    let domain = domain.ok_or_else(|| {
200        syn::Error::new_spanned(attr, "#[slot(...)] missing required `domain = \"...\"`")
201    })?;
202    let kind_ident = kind_ident.ok_or_else(|| {
203        syn::Error::new_spanned(attr, "#[slot(...)] missing required `kind = ...`")
204    })?;
205
206    Ok(SlotAttr {
207        domain,
208        kind_ident,
209        key_domain,
210    })
211}
212
213pub(crate) fn parse_expr_attr(attr: &syn::Attribute, field_ident: &Ident) -> syn::Result<ExprAttr> {
214    let tokens = match &attr.meta {
215        Meta::List(list) => list.tokens.clone(),
216        other => {
217            return Err(syn::Error::new_spanned(
218                other,
219                "#[expr(...)] must use parenthesized form",
220            ));
221        }
222    };
223
224    let parsed = syn::parse2::<ExprAttrInner>(tokens)?;
225
226    Ok(ExprAttr {
227        name: parsed.name,
228        field_type_ident: parsed.field_type_ident,
229        eval_kind_ident: parsed.eval_kind_ident,
230        field_ident: field_ident.clone(),
231    })
232}
233
234struct ExprAttrInner {
235    name: LitStr,
236    field_type_ident: Ident,
237    eval_kind_ident: Ident,
238}
239
240impl syn::parse::Parse for ExprAttrInner {
241    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
242        let name: LitStr = input.parse()?;
243
244        input.parse::<Comma>()?;
245        let field_type_ident: Ident = input.parse()?;
246
247        input.parse::<Comma>()?;
248        let eval_kind_ident: Ident = input.parse()?;
249        let _ = input.parse::<Option<Comma>>()?;
250
251        // Variant existence is left to the consumer compiler; idents retain the user's spans.
252
253        Ok(Self {
254            name,
255            field_type_ident,
256            eval_kind_ident,
257        })
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use googletest::prelude::*;
264
265    use super::*;
266
267    #[gtest]
268    fn define_slot_rejects_a_duplicate_impl_section() -> Result<()> {
269        let error = syn::parse2::<DefineSlotInput>(quote::quote! {
270            struct Slot { value: f64 }
271            impl {}
272            impl {}
273        })
274        .err()
275        .or_fail()?;
276
277        verify_that!(
278            error.to_string(),
279            eq("duplicate `impl` section in define_slot!")
280        )
281    }
282
283    #[gtest]
284    fn slot_attribute_rejects_a_duplicate_domain_key() -> Result<()> {
285        let item: ItemStruct = syn::parse_quote! {
286            #[slot(domain = "first", domain = "second", kind = singleton)]
287            struct Slot { value: f64 }
288        };
289
290        let error = parse_slot_attr(&item.attrs[0]).err().or_fail()?;
291
292        verify_that!(error.to_string(), eq("duplicate `domain` slot attribute"))
293    }
294
295    #[gtest]
296    fn trait_support_attribute_requires_exactly_one_crate_path() -> Result<()> {
297        let missing: DeriveInput = syn::parse_quote! {
298            #[has_id()]
299            struct Record { ID: i32 }
300        };
301        let duplicate: DeriveInput = syn::parse_quote! {
302            #[has_id(crate = "support", crate = "other")]
303            struct Record { ID: i32 }
304        };
305
306        let missing_error = parse_trait_crate_attr(&missing, "has_id").err().or_fail()?;
307        let duplicate_error = parse_trait_crate_attr(&duplicate, "has_id")
308            .err()
309            .or_fail()?;
310
311        verify_that!(
312            missing_error.to_string(),
313            eq("#[has_id(...)] missing required `crate = \"...\"`")
314        )?;
315
316        verify_that!(
317            duplicate_error.to_string(),
318            eq("duplicate `has_id` crate path")
319        )
320    }
321}