// SPDX-License-Identifier: (BSD-2-Clause OR Apache-2.0) OR MIT // // Copyright 2026 The Fuchsia Authors // // Licensed under a BSD-style license , Apache License, Version 2.0 // , or the MIT // license , at your option. // This file may not be copied, modified, or distributed except according to // those terms. use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::{quote, ToTokens as _}; use syn::{ punctuated::Punctuated, spanned::Spanned as _, Attribute, Data, DeriveInput, Error, Expr, Field, MacroDelimiter, Meta, Token, }; use crate::util::path_is_ident; /// Parses field invariants in attribute order. pub(crate) fn parse(attrs: &[Attribute]) -> Result, Error> { let mut invariants = Vec::new(); for attr in attrs.iter().filter(|attr| path_is_ident(attr.path(), "zerocopy")) { let options = attr.parse_args_with(Punctuated::::parse_terminated)?; for option in options { if !path_is_ident(option.path(), "invariant") { return Err(Error::new_spanned(option, "expected `invariant(...)`")); } match option { Meta::List(list) if matches!(list.delimiter, MacroDelimiter::Paren(_)) => { invariants.push(list.parse_args::()?) } other => return Err(Error::new_spanned(other, "expected `invariant(...)`")), } } } Ok(invariants) } /// Validates invariant syntax and placement, returning the first invariant's /// span so derives which cannot honor invariants can reject them. pub(crate) fn validate(ast: &DeriveInput) -> Result, Error> { let fields: Vec<&Field> = match &ast.data { Data::Struct(data) => data.fields.iter().collect(), Data::Enum(data) => { for variant in &data.variants { if let Some(invariant) = parse(&variant.attrs)?.first() { return Err(Error::new( invariant.span(), "invariants are only supported on named fields", )); } } data.variants.iter().flat_map(|variant| &variant.fields).collect() } Data::Union(data) => data.fields.named.iter().collect(), }; let mut first = None; for field in fields { if let Some(invariant) = parse(&field.attrs)?.first() { if field.ident.is_none() { return Err(Error::new( invariant.span(), "invariants are only supported on named fields", )); } first.get_or_insert(invariant.span()); } } Ok(first) } /// Visits identifiers, including those inside macro arguments. pub(crate) fn idents(tokens: TokenStream) -> Vec { tokens .into_iter() .flat_map(|token| match token { TokenTree::Ident(ident) => vec![ident], TokenTree::Group(group) => idents(group.stream()), _ => Vec::new(), }) .collect() } /// Binds semantic field names in every syntax context used by the expression. /// Preserve the expression's tokens so its other names retain their hygiene. pub(crate) fn bind_fields(fields: &[(&Field, Ident)], expression: &Expr) -> TokenStream { let mut body = expression.to_token_stream(); let references = idents(body.clone()); for (field, binding) in fields { let name = match &field.ident { Some(name) => name, None => continue, }; let spelling = name.to_string(); let spelling = spelling.trim_start_matches("r#"); // Also support references produced by a macro invoked by the expression. let mut call_site = name.clone(); call_site.set_span(Span::call_site()); for alias in [name.clone(), call_site].into_iter().chain( references .iter() .filter(|ident| ident.to_string().trim_start_matches("r#") == spelling) .cloned(), ) { body = quote! {{ // Require a binding; reject names Rust would resolve as constant // patterns. Item aliases could capture helper calls in macros. #[allow(unused_variables, non_snake_case, clippy::redundant_pattern)] let #alias @ _ = #binding; #body }}; } } body }