xref: /linux/rust/zerocopy-derive/invariant.rs (revision 7c306f00835a237a70024a8f968dbc3bc8db8fcf)
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 
11 use proc_macro2::{Ident, Span, TokenStream, TokenTree};
12 use quote::{quote, ToTokens as _};
13 use syn::{
14     punctuated::Punctuated, spanned::Spanned as _, Attribute, Data, DeriveInput, Error, Expr,
15     Field, MacroDelimiter, Meta, Token,
16 };
17 
18 use crate::util::path_is_ident;
19 
20 /// Parses field invariants in attribute order.
21 pub(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.
42 pub(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.
74 pub(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.
87 pub(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 }
117