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