structural_assert/
lib.rs

1// Copyright 2021 Gregory Oakes
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21#![allow(dead_code)]
22
23use core::iter::repeat;
24use proc_macro::TokenStream;
25use quote::quote;
26use syn::{
27    braced,
28    parse::{Parse, ParseStream},
29    parse_macro_input,
30    punctuated::Punctuated,
31    spanned::Spanned,
32    token, Attribute, Field, Ident, Lit, LitInt, Meta, NestedMeta, Token, Visibility,
33};
34
35struct Item {
36    attrs: Vec<Attribute>,
37    vis: Visibility,
38    struct_token: Token![struct],
39    ident: Ident,
40    brace_token: token::Brace,
41    fields: Punctuated<Field, Token![,]>,
42}
43
44impl Parse for Item {
45    fn parse(input: ParseStream) -> syn::Result<Self> {
46        let attrs = input.call(Attribute::parse_outer)?;
47        let vis = input.parse()?;
48        let lookahead = input.lookahead1();
49        if lookahead.peek(Token![struct]) {
50            let content;
51            Ok(Item {
52                attrs,
53                vis,
54                struct_token: input.parse()?,
55                ident: input.parse()?,
56                brace_token: braced!(content in input),
57                fields: content.parse_terminated(Field::parse_named)?,
58            })
59        } else {
60            Err(lookahead.error())
61        }
62    }
63}
64
65struct Assertion {
66    start: LitInt,
67    colon: token::Colon,
68    end: LitInt,
69}
70
71impl Parse for Assertion {
72    fn parse(input: ParseStream) -> syn::Result<Self> {
73        Ok(Assertion {
74            start: input.parse()?,
75            colon: input.parse()?,
76            end: input.parse()?,
77        })
78    }
79}
80
81#[proc_macro_attribute]
82pub fn test_structure(attrs: TokenStream, tokens: TokenStream) -> TokenStream {
83    let Item {
84        attrs: struct_attrs,
85        vis,
86        ident,
87        mut fields,
88        ..
89    } = parse_macro_input!(tokens as Item);
90    let meta = parse_macro_input!(attrs as NestedMeta);
91    let size = match meta {
92        NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("size") => match &nv.lit {
93            Lit::Int(tok) => quote! {
94                assert_eq!(#tok, ::core::mem::size_of::<#ident>(), "size of {}", ::core::stringify!(#ident));
95            },
96            tok => syn::Error::new(tok.span(), "Unexpected size type").to_compile_error(),
97        },
98        m => syn::Error::new(m.span(), "Unexpected meta item").to_compile_error(),
99    };
100    let assertions = fields
101        .iter()
102        .flat_map(|field| {
103            field
104                .attrs
105                .iter()
106                .filter(|attr| attr.path.is_ident("loc"))
107                .zip(repeat(field))
108        })
109        .fold(quote! {}, |acc, (attr, field)| {
110            let assertion = if let Some(field_ident) = &field.ident {
111                let typ = &field.ty;
112                match attr.parse_args() {
113                    Ok(Assertion { start, end, .. }) => quote! {
114                        {
115                            let desc = format!(
116                                "{}.{} ({})",
117                                ::core::stringify!(#ident),
118                                ::core::stringify!(#field_ident),
119                                ::core::stringify!(#typ),
120                            );
121                            let offset = ::memoffset::offset_of!(#ident, #field_ident);
122                            assert_eq!(#start, offset, "start of {}", desc);
123                            assert_eq!(#end, ::core::mem::size_of::<#typ>() + offset - 1, "end of {}", desc);
124                        }
125                    },
126                    Err(e) => e.to_compile_error(),
127                }
128            } else {
129                syn::Error::new(field.span(), "Tuple structs are not supported").to_compile_error()
130            };
131            quote! { #acc #assertion; }
132        });
133    let func_name = Ident::new(format!("structure_{}", ident).as_str(), ident.span());
134    for field in fields.iter_mut() {
135        field.attrs.retain(|attr| !attr.path.is_ident("loc"));
136    }
137    let struct_attrs_stream = struct_attrs
138        .iter()
139        .fold(quote! {}, |acc, attr| quote! { #acc #attr });
140    let output = quote! {
141        #struct_attrs_stream
142        #vis struct #ident {
143            #fields
144        }
145
146        #[cfg(test)]
147        #[allow(non_snake_case)]
148        #[test]
149        fn #func_name() {
150            #assertions
151            #size
152        }
153    };
154    output.into()
155}