Skip to main content

moverox_codegen/
attributes.rs

1use std::collections::HashSet;
2
3use move_syn::Attributes;
4use quote::quote;
5use unsynn::{CommaDelimitedVec, IParse as _, Ident, ToTokens as _, TokenStream};
6
7use crate::Result;
8
9#[expect(clippy::result_large_err, reason = "Error from the unsynn crate")]
10mod grammar {
11    use unsynn::*;
12
13    mod kw {
14        use unsynn::unsynn;
15
16        unsynn! {
17            pub(super) keyword Moverox = "moverox";
18            pub(super) keyword Otw = "OTW";
19            // NOTE: we cannot use `type` here since the Move parser will complain because it's a
20            // reserved keyword
21            pub(super) keyword Type = "type_";
22        }
23    }
24
25    unsynn! {
26        /// Allows parsing `moverox(...)` attributes inside `#[ext(...)]`
27        ///
28        /// # Example
29        ///
30        /// ```move
31        /// #[ext(moverox(type(T = OTW)))]
32        /// public struct BalanceUpdated<phantom T> {}
33        /// ```
34        pub(crate) struct Annotation {
35            kw: kw::Moverox,
36            contents: ParenthesisGroupContaining<CommaDelimitedVec<Setting>>
37        }
38
39        /// The different accepted attributes inside `moverox(...)`
40        pub(super) enum Setting {
41            /// Currrently only type defaults
42            Type(Type)
43        }
44
45        /// Custom attribute to set defaults for type parameters of a datatype.
46        pub(super) struct Type {
47            kw: kw::Type,
48            contents: ParenthesisGroupContaining<CommaDelimitedVec<TypeDefault>>,
49        }
50
51        /// An instance of a type parameter default.
52        struct TypeDefault {
53            /// Identifier of the annotated datatype's type parameter
54            ident: Ident,
55            assign: Assign,
56            /// For now, only defaults like `{T} = OTW` are supported
57            default: kw::Otw,
58        }
59
60        /// A single comma-separated entry inside an `#[ext(...)]` group.
61        ///
62        /// `#[ext(...)]` is a shared namespace: other tooling attaches its own sub-attributes
63        /// (e.g. `versioned(...)`, `dynamic_field(...)`) alongside `moverox(...)`. We parse the
64        /// whole group and keep only the `moverox(...)` entries, ignoring the rest.
65        pub(crate) enum ExtEntry {
66            Moverox(Annotation),
67            Other(OtherEntry),
68        }
69
70        /// Any non-`moverox` `#[ext(...)]` entry. Consumes everything up to the next comma,
71        /// so it accepts every shape other tooling might use — `versioned(EFoo)`,
72        /// `dynamic_field(name = ID, value = ID)`, `foo = b"bar"`, a bare `dev_inspect`, …
73        pub(crate) struct OtherEntry {
74            tokens: Vec<Cons<Except<Comma>, TokenTree>>,
75        }
76    }
77
78    impl Annotation {
79        pub(super) fn settings(&self) -> impl Iterator<Item = &Setting> + '_ {
80            self.contents
81                .content
82                .iter()
83                .map(|delimited| &delimited.value)
84        }
85    }
86
87    impl Setting {
88        pub(super) fn otw_types(&self) -> impl Iterator<Item = &Ident> + '_ {
89            let Self::Type(ty) = self;
90            ty.contents
91                .content
92                .iter()
93                .map(|delimited| &delimited.value.ident)
94        }
95    }
96}
97
98/// Filter and parse Move attributes into Rust docs (1st) and OTW type defaults (2nd).
99pub(super) fn extract(attrs: &[Attributes]) -> Result<(TokenStream, HashSet<Ident>)> {
100    let (move_docs, other): (Vec<_>, Vec<_>) = attrs.iter().partition(|attr| attr.is_doc());
101
102    let rust_docs = move_docs.into_iter().map(process_doc).collect();
103
104    let custom: Vec<_> = other.into_iter().flat_map(as_moverox).collect();
105    let mut otw_types = HashSet::new();
106    for ident in custom
107        .iter()
108        .flat_map(|custom| custom.settings())
109        .flat_map(|setting| setting.otw_types())
110    {
111        if otw_types.contains(ident) {
112            return Err(format!("Type {ident} declared twice").into());
113        }
114        otw_types.insert(ident.to_owned());
115    }
116
117    Ok((rust_docs, otw_types))
118}
119
120pub(super) fn as_moverox(attr: &Attributes) -> impl Iterator<Item = self::grammar::Annotation> {
121    // An `#[ext(...)]` group may carry sibling sub-attributes from other tooling
122    // (e.g. `#[ext(moverox(type_(T = OTW)), versioned(EFoo))]`). Parse the whole
123    // comma-delimited group and keep only the `moverox(...)` entries, so a sibling
124    // attribute never causes the `moverox(...)` annotation to be dropped.
125    attr.external_attributes()
126        .filter_map(|ext| {
127            ext.to_token_iter()
128                .parse_all::<CommaDelimitedVec<self::grammar::ExtEntry>>()
129                .ok()
130        })
131        .flat_map(|entries| {
132            entries.into_iter().filter_map(|entry| match entry.value {
133                self::grammar::ExtEntry::Moverox(annotation) => Some(annotation),
134                self::grammar::ExtEntry::Other(_) => None,
135            })
136        })
137}
138
139fn process_doc(attr: &Attributes) -> TokenStream {
140    let inner = attr.contents().to_token_stream();
141    // NOTE: disable when compiling doctests to avoid Rust interpreting code blocks as
142    // runnable tests.
143    quote!(#[cfg_attr(not(doctest), #inner)])
144}