Skip to main content

zerocopy_derive/
invariant.rs

1// SPDX-License-Identifier: (BSD-2-Clause OR Apache-2.0) OR MIT
2//
3// Copyright 2026 The Fuchsia Authors
4//
5// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
6// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
7// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
8// This file may not be copied, modified, or distributed except according to
9// those terms.
10
11use proc_macro2::{Ident, Span, TokenStream, TokenTree};
12use quote::{quote, ToTokens as _};
13use syn::{
14    punctuated::Punctuated, spanned::Spanned as _, Attribute, Data, DeriveInput, Error, Expr,
15    Field, MacroDelimiter, Meta, Token,
16};
17
18use crate::util::path_is_ident;
19
20/// Parses field invariants in attribute order.
21pub(crate) fn parse(attrs: &[Attribute]) -> Result<Vec<Expr>, Error> {
22    let mut invariants = Vec::new();
23    for attr in attrs.iter().filter(|attr| path_is_ident(attr.path(), "zerocopy")) {
24        let options = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
25        for option in options {
26            if !path_is_ident(option.path(), "invariant") {
27                return Err(Error::new_spanned(option, "expected `invariant(...)`"));
28            }
29            match option {
30                Meta::List(list) if matches!(list.delimiter, MacroDelimiter::Paren(_)) => {
31                    invariants.push(list.parse_args::<Expr>()?)
32                }
33                other => return Err(Error::new_spanned(other, "expected `invariant(...)`")),
34            }
35        }
36    }
37    Ok(invariants)
38}
39
40/// Validates invariant syntax and placement, returning the first invariant's
41/// span so derives which cannot honor invariants can reject them.
42pub(crate) fn validate(ast: &DeriveInput) -> Result<Option<Span>, Error> {
43    let fields: Vec<&Field> = match &ast.data {
44        Data::Struct(data) => data.fields.iter().collect(),
45        Data::Enum(data) => {
46            for variant in &data.variants {
47                if let Some(invariant) = parse(&variant.attrs)?.first() {
48                    return Err(Error::new(
49                        invariant.span(),
50                        "invariants are only supported on named fields",
51                    ));
52                }
53            }
54            data.variants.iter().flat_map(|variant| &variant.fields).collect()
55        }
56        Data::Union(data) => data.fields.named.iter().collect(),
57    };
58    let mut first = None;
59    for field in fields {
60        if let Some(invariant) = parse(&field.attrs)?.first() {
61            if field.ident.is_none() {
62                return Err(Error::new(
63                    invariant.span(),
64                    "invariants are only supported on named fields",
65                ));
66            }
67            first.get_or_insert(invariant.span());
68        }
69    }
70    Ok(first)
71}
72
73/// Visits identifiers, including those inside macro arguments.
74pub(crate) fn idents(tokens: TokenStream) -> Vec<Ident> {
75    tokens
76        .into_iter()
77        .flat_map(|token| match token {
78            TokenTree::Ident(ident) => vec![ident],
79            TokenTree::Group(group) => idents(group.stream()),
80            _ => Vec::new(),
81        })
82        .collect()
83}
84
85/// Binds semantic field names in every syntax context used by the expression.
86/// Preserve the expression's tokens so its other names retain their hygiene.
87pub(crate) fn bind_fields(fields: &[(&Field, Ident)], expression: &Expr) -> TokenStream {
88    let mut body = expression.to_token_stream();
89    let references = idents(body.clone());
90    for (field, binding) in fields {
91        let name = match &field.ident {
92            Some(name) => name,
93            None => continue,
94        };
95        let spelling = name.to_string();
96        let spelling = spelling.trim_start_matches("r#");
97        // Also support references produced by a macro invoked by the expression.
98        let mut call_site = name.clone();
99        call_site.set_span(Span::call_site());
100        for alias in [name.clone(), call_site].into_iter().chain(
101            references
102                .iter()
103                .filter(|ident| ident.to_string().trim_start_matches("r#") == spelling)
104                .cloned(),
105        ) {
106            body = quote! {{
107                // Require a binding; reject names Rust would resolve as constant
108                // patterns. Item aliases could capture helper calls in macros.
109                #[allow(unused_variables, non_snake_case, clippy::redundant_pattern)]
110                let #alias @ _ = #binding;
111                #body
112            }};
113        }
114    }
115    body
116}