Skip to main content

servo_dom_struct/
lib.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5#![recursion_limit = "128"]
6
7use proc_macro::TokenStream;
8use quote::quote;
9use syn::*;
10mod domobject;
11use crate::domobject::expand_dom_object;
12
13#[proc_macro_attribute]
14pub fn dom_struct(args: TokenStream, input: TokenStream) -> TokenStream {
15    let args2 = proc_macro2::TokenStream::from(args);
16    let input2 = proc_macro2::TokenStream::from(input);
17
18    TokenStream::from(dom_struct_impl(args2, input2))
19}
20
21fn dom_struct_impl(
22    args: proc_macro2::TokenStream,
23    input: proc_macro2::TokenStream,
24) -> proc_macro2::TokenStream {
25    let associated_memory = args.to_string().contains("associated_memory");
26    if !associated_memory && !args.is_empty() {
27        panic!("#[dom_struct] only takes 'associated_memory' as an argument");
28    }
29    let attributes = quote! {
30        #[derive(deny_public_fields::DenyPublicFields, JSTraceable, MallocSizeOf)]
31        #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
32        #[repr(C)]
33    };
34
35    // Work around https://github.com/rust-lang/rust/issues/46489
36    let attributes: proc_macro2::TokenStream = attributes.to_string().parse().unwrap();
37
38    let output: proc_macro2::TokenStream = attributes.into_iter().chain(input).collect();
39
40    let item: Item = syn::parse2(output).unwrap();
41
42    if let Item::Struct(s) = item {
43        let expanded_dom_object = expand_dom_object(s.clone(), associated_memory);
44        let s2 = quote! { #s #expanded_dom_object };
45        if !s.generics.params.is_empty() {
46            return s2;
47        }
48        if let Fields::Named(ref f) = s.fields {
49            let f = f.named.first().expect("Must have at least one field");
50            let ident = f.ident.as_ref().expect("Must have named fields");
51            let name = &s.ident;
52            let ty = &f.ty;
53
54            quote! (
55                #s2
56
57                impl crate::HasParent for #name {
58                    type Parent = #ty;
59                    /// This is used in a type assertion to ensure that
60                    /// the source and webidls agree as to what the parent type is
61                    fn as_parent(&self) -> &#ty {
62                        &self.#ident
63                    }
64                }
65            )
66        } else {
67            panic!("#[dom_struct] only applies to structs with named fields");
68        }
69    } else {
70        panic!("#[dom_struct] only applies to structs");
71    }
72}
73
74#[test]
75fn test_valid_dom_struct_generation() {
76    let args = quote! { associated_memory };
77    let reflector_type: syn::Type = parse_quote!(Reflector);
78    let input = quote! {
79        struct DomElement {
80            reflector: #reflector_type,
81        }
82    };
83
84    let result = dom_struct_impl(args, input);
85
86    let output =
87        syn::parse2(result).expect("Macro output failed to parse into a valid Rust file structure");
88    let formatted_output = prettyplease::unparse(&output);
89
90    let expected_output = quote! {
91        #[derive(deny_public_fields::DenyPublicFields, JSTraceable, MallocSizeOf)]
92        #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
93        #[repr(C)]
94        struct DomElement {
95            reflector: Reflector,
96        }
97        #[expect(non_upper_case_globals)]
98        const _IMPL_DOMOBJECT_FOR_DomElement: () = {
99            trait NoDomObjectInDomObject<A> {
100                fn some_item() {}
101            }
102            impl<T: ?Sized> NoDomObjectInDomObject<()> for T {}
103            #[expect(dead_code)]
104            struct Invalid;
105            impl<T> NoDomObjectInDomObject<Invalid> for T
106            where
107                T: ?Sized + crate::DomObject,
108            {}
109        };
110        impl ::js::conversions::ToJSValConvertible for DomElement {
111            #[expect(unsafe_code)]
112            unsafe fn to_jsval(
113                &self,
114                cx: *mut js::jsapi::JSContext,
115                rval: js::rust::MutableHandleValue,
116            ) {
117                let object = crate::DomObject::reflector(self).get_jsobject();
118                object.to_jsval(cx, rval)
119            }
120        }
121        impl crate::DomObject for DomElement {
122            type ReflectorType = crate::AssociatedMemory;
123            #[inline]
124            fn reflector(&self) -> &crate::Reflector<Self::ReflectorType> {
125                self.reflector.reflector()
126            }
127        }
128        impl crate::MutDomObject for DomElement {
129            unsafe fn init_reflector<Actual>(&self, obj: *mut js::jsapi::JSObject) {
130                self.reflector.init_reflector::<Actual>(obj);
131            }
132            unsafe fn init_reflector_without_associated_memory(
133                &self,
134                obj: *mut js::jsapi::JSObject,
135            ) {
136                self.reflector.init_reflector_without_associated_memory(obj);
137            }
138        }
139        impl Eq for DomElement {}
140        impl PartialEq for DomElement {
141            fn eq(&self, other: &Self) -> bool {
142                crate::DomObject::reflector(self) == crate::DomObject::reflector(other)
143            }
144        }
145        impl crate::HasParent for DomElement {
146            type Parent = Reflector;
147            /// This is used in a type assertion to ensure that
148            /// the source and webidls agree as to what the parent type is
149            fn as_parent(&self) -> &Reflector {
150                &self.reflector
151            }
152        }
153    };
154    let expected_output_parsed: syn::File = syn::parse2(expected_output)
155        .expect("Macro output failed to parse into a valid Rust file structure");
156    let expected_formatted_output = prettyplease::unparse(&expected_output_parsed);
157
158    assert_eq!(
159        formatted_output.to_string(),
160        expected_formatted_output.to_string()
161    )
162}
163
164#[test]
165#[should_panic(expected = "#[dom_struct] only takes 'associated_memory'")]
166fn test_invalid_arguments_panic() {
167    let args = quote! { invalid_flag_here };
168    let input = quote! { struct MockStruct { first_field: i32 } };
169
170    dom_struct_impl(args, input);
171}
172
173#[test]
174#[should_panic(expected = "#[dom_struct] should not be applied on empty structs")]
175fn test_empty_struct_panic() {
176    let args = quote! {};
177    let input = quote! { struct EmptyStruct{} };
178
179    dom_struct_impl(args, input);
180}