alloy_sol_macro_input/
attr.rs

1use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
2use proc_macro2::TokenStream;
3use quote::quote;
4use syn::{
5    meta::ParseNestedMeta, parse::Parse, punctuated::Punctuated, Attribute, Error, LitBool, LitStr,
6    Path, Result, Token,
7};
8
9const DUPLICATE_ERROR: &str = "duplicate attribute";
10const UNKNOWN_ERROR: &str = "unknown `sol` attribute";
11
12/// Wraps the argument in a doc attribute.
13pub fn mk_doc(s: impl quote::ToTokens) -> TokenStream {
14    quote!(#[doc = #s])
15}
16
17/// Returns `true` if the attribute is `#[doc = "..."]`.
18pub fn is_doc(attr: &Attribute) -> bool {
19    attr.path().is_ident("doc")
20}
21
22/// Returns `true` if the attribute is `#[derive(...)]`.
23pub fn is_derive(attr: &Attribute) -> bool {
24    attr.path().is_ident("derive")
25}
26
27/// Returns an iterator over all the `#[doc = "..."]` attributes.
28pub fn docs(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
29    attrs.iter().filter(|a| is_doc(a))
30}
31
32/// Flattens all the `#[doc = "..."]` attributes into a single string.
33pub fn docs_str(attrs: &[Attribute]) -> String {
34    let mut doc = String::new();
35    for attr in docs(attrs) {
36        let syn::Meta::NameValue(syn::MetaNameValue {
37            value: syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }),
38            ..
39        }) = &attr.meta
40        else {
41            continue;
42        };
43
44        let value = s.value();
45        if !value.is_empty() {
46            if !doc.is_empty() {
47                doc.push('\n');
48            }
49            doc.push_str(&value);
50        }
51    }
52    doc
53}
54
55/// Returns an iterator over all the `#[derive(...)]` attributes.
56pub fn derives(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
57    attrs.iter().filter(|a| is_derive(a))
58}
59
60/// Returns an iterator over all the rust `::` paths in the `#[derive(...)]`
61/// attributes.
62pub fn derives_mapped(attrs: &[Attribute]) -> impl Iterator<Item = Path> + '_ {
63    derives(attrs).flat_map(parse_derives)
64}
65
66/// Parses the `#[derive(...)]` attributes into a list of paths.
67pub fn parse_derives(attr: &Attribute) -> Punctuated<Path, Token![,]> {
68    attr.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated).unwrap_or_default()
69}
70
71// When adding a new attribute:
72// 1. add a field to this struct,
73// 2. add a match arm in the `parse` function below,
74// 3. add test cases in the `tests` module at the bottom of this file,
75// 4. implement the attribute in your `SolInputExpander` implementation,
76// 5. document the attribute in the [`sol!`] macro docs.
77
78/// `#[sol(...)]` attributes.
79#[derive(Debug, Default, PartialEq, Eq)]
80pub struct SolAttrs {
81    /// `#[sol(rpc)]`
82    pub rpc: Option<bool>,
83    /// `#[sol(abi)]`
84    pub abi: Option<bool>,
85    /// `#[sol(all_derives)]`
86    pub all_derives: Option<bool>,
87    /// `#[sol(extra_derives(...))]`
88    pub extra_derives: Option<Vec<Path>>,
89    /// `#[sol(extra_methods)]`
90    pub extra_methods: Option<bool>,
91    /// `#[sol(docs)]`
92    pub docs: Option<bool>,
93
94    /// `#[sol(alloy_sol_types = alloy_core::sol_types)]`
95    pub alloy_sol_types: Option<Path>,
96    /// `#[sol(alloy_contract = alloy_contract)]`
97    pub alloy_contract: Option<Path>,
98
99    // TODO: Implement
100    /// UNIMPLEMENTED: `#[sol(rename = "new_name")]`
101    pub rename: Option<LitStr>,
102    // TODO: Implement
103    /// UNIMPLEMENTED: `#[sol(rename_all = "camelCase")]`
104    pub rename_all: Option<CasingStyle>,
105
106    /// `#[sol(bytecode = "0x1234")]`
107    pub bytecode: Option<LitStr>,
108    /// `#[sol(deployed_bytecode = "0x1234")]`
109    pub deployed_bytecode: Option<LitStr>,
110
111    /// UDVT only `#[sol(type_check = "my_function")]`
112    pub type_check: Option<LitStr>,
113}
114
115impl SolAttrs {
116    /// Parse the `#[sol(...)]` attributes from a list of attributes.
117    pub fn parse(attrs: &[Attribute]) -> Result<(Self, Vec<Attribute>)> {
118        let mut this = Self::default();
119        let mut others = Vec::with_capacity(attrs.len());
120        for attr in attrs {
121            if !attr.path().is_ident("sol") {
122                others.push(attr.clone());
123                continue;
124            }
125
126            attr.meta.require_list()?.parse_nested_meta(|meta| {
127                let path = meta.path.get_ident().ok_or_else(|| meta.error("expected ident"))?;
128                let s = path.to_string();
129
130                macro_rules! match_ {
131                    ($($l:ident => $e:expr),* $(,)?) => {
132                        match s.as_str() {
133                            $(
134                                stringify!($l) => if this.$l.is_some() {
135                                    return Err(meta.error(DUPLICATE_ERROR))
136                                } else {
137                                    this.$l = Some($e);
138                                },
139                            )*
140                            _ => return Err(meta.error(UNKNOWN_ERROR)),
141                        }
142                    };
143                }
144
145                // `path` => true, `path = <bool>` => <bool>
146                let bool = || {
147                    if let Ok(input) = meta.value() {
148                        input.parse::<LitBool>().map(|lit| lit.value)
149                    } else {
150                        Ok(true)
151                    }
152                };
153
154                // `path = <path>`
155                let path = || meta.value()?.parse::<Path>();
156
157                // `path = "<str>"`
158                let lit = || {
159                    let value = meta.value()?;
160                    let span = value.span();
161                    let macro_string::MacroString(value) =
162                        value.parse::<macro_string::MacroString>()?;
163                    Ok::<_, syn::Error>(LitStr::new(&value, span))
164                };
165
166                // `path = "0x<hex>"`
167                let bytes = || {
168                    let lit = lit()?;
169                    if let Err(e) = hex::check(lit.value()) {
170                        let msg = format!("invalid hex value: {e}");
171                        return Err(Error::new(lit.span(), msg));
172                    }
173                    Ok(lit)
174                };
175
176                // `path(comma, separated, list)`
177                fn list<T>(
178                    meta: &ParseNestedMeta<'_>,
179                    parser: fn(syn::parse::ParseStream<'_>) -> Result<T>,
180                ) -> Result<Vec<T>> {
181                    let content;
182                    syn::parenthesized!(content in meta.input);
183                    Ok(content.parse_terminated(parser, Token![,])?.into_iter().collect())
184                }
185
186                match_! {
187                    rpc => bool()?,
188                    abi => bool()?,
189                    all_derives => bool()?,
190                    extra_derives => list(&meta, Path::parse)?,
191                    extra_methods => bool()?,
192                    docs => bool()?,
193
194                    alloy_sol_types => path()?,
195                    alloy_contract => path()?,
196
197                    rename => lit()?,
198                    rename_all => CasingStyle::from_lit(&lit()?)?,
199
200                    bytecode => bytes()?,
201                    deployed_bytecode => bytes()?,
202
203                    type_check => lit()?,
204                };
205                Ok(())
206            })?;
207        }
208        Ok((this, others))
209    }
210}
211
212/// Trait for items that contain `#[sol(...)]` attributes among other
213/// attributes. This is usually a shortcut  for [`SolAttrs::parse`].
214pub trait ContainsSolAttrs {
215    /// Get the list of attributes.
216    fn attrs(&self) -> &[Attribute];
217
218    /// Parse the `#[sol(...)]` attributes from the list of attributes.
219    fn split_attrs(&self) -> syn::Result<(SolAttrs, Vec<Attribute>)> {
220        SolAttrs::parse(self.attrs())
221    }
222}
223
224impl ContainsSolAttrs for syn_solidity::File {
225    fn attrs(&self) -> &[Attribute] {
226        &self.attrs
227    }
228}
229
230impl ContainsSolAttrs for syn_solidity::ItemContract {
231    fn attrs(&self) -> &[Attribute] {
232        &self.attrs
233    }
234}
235
236impl ContainsSolAttrs for syn_solidity::ItemEnum {
237    fn attrs(&self) -> &[Attribute] {
238        &self.attrs
239    }
240}
241
242impl ContainsSolAttrs for syn_solidity::ItemError {
243    fn attrs(&self) -> &[Attribute] {
244        &self.attrs
245    }
246}
247
248impl ContainsSolAttrs for syn_solidity::ItemEvent {
249    fn attrs(&self) -> &[Attribute] {
250        &self.attrs
251    }
252}
253
254impl ContainsSolAttrs for syn_solidity::ItemFunction {
255    fn attrs(&self) -> &[Attribute] {
256        &self.attrs
257    }
258}
259
260impl ContainsSolAttrs for syn_solidity::ItemStruct {
261    fn attrs(&self) -> &[Attribute] {
262        &self.attrs
263    }
264}
265
266impl ContainsSolAttrs for syn_solidity::ItemUdt {
267    fn attrs(&self) -> &[Attribute] {
268        &self.attrs
269    }
270}
271
272/// Defines the casing for the attributes long representation.
273#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum CasingStyle {
275    /// Indicate word boundaries with uppercase letter, excluding the first
276    /// word.
277    Camel,
278    /// Keep all letters lowercase and indicate word boundaries with hyphens.
279    Kebab,
280    /// Indicate word boundaries with uppercase letter, including the first
281    /// word.
282    Pascal,
283    /// Keep all letters uppercase and indicate word boundaries with
284    /// underscores.
285    ScreamingSnake,
286    /// Keep all letters lowercase and indicate word boundaries with
287    /// underscores.
288    Snake,
289    /// Keep all letters lowercase and remove word boundaries.
290    Lower,
291    /// Keep all letters uppercase and remove word boundaries.
292    Upper,
293    /// Use the original attribute name defined in the code.
294    Verbatim,
295}
296
297impl CasingStyle {
298    fn from_lit(name: &LitStr) -> Result<Self> {
299        let normalized = name.value().to_upper_camel_case().to_lowercase();
300        let s = match normalized.as_ref() {
301            "camel" | "camelcase" => Self::Camel,
302            "kebab" | "kebabcase" => Self::Kebab,
303            "pascal" | "pascalcase" => Self::Pascal,
304            "screamingsnake" | "screamingsnakecase" => Self::ScreamingSnake,
305            "snake" | "snakecase" => Self::Snake,
306            "lower" | "lowercase" => Self::Lower,
307            "upper" | "uppercase" => Self::Upper,
308            "verbatim" | "verbatimcase" => Self::Verbatim,
309            s => return Err(Error::new(name.span(), format!("unsupported casing: {s}"))),
310        };
311        Ok(s)
312    }
313
314    /// Apply the casing style to the given string.
315    #[allow(dead_code)]
316    pub fn apply(self, s: &str) -> String {
317        match self {
318            Self::Pascal => s.to_upper_camel_case(),
319            Self::Kebab => s.to_kebab_case(),
320            Self::Camel => s.to_lower_camel_case(),
321            Self::ScreamingSnake => s.to_shouty_snake_case(),
322            Self::Snake => s.to_snake_case(),
323            Self::Lower => s.to_snake_case().replace('_', ""),
324            Self::Upper => s.to_shouty_snake_case().replace('_', ""),
325            Self::Verbatim => s.to_owned(),
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use syn::parse_quote;
334
335    macro_rules! test_sol_attrs {
336        ($($(#[$attr:meta])* $group:ident { $($t:tt)* })+) => {$(
337            #[test]
338            $(#[$attr])*
339            fn $group() {
340                test_sol_attrs! { $($t)* }
341            }
342        )+};
343
344        ($( $(#[$attr:meta])* => $expected:expr ),+ $(,)?) => {$(
345            run_test(
346                &[$(stringify!(#[$attr])),*],
347                $expected
348            );
349        )+};
350    }
351
352    macro_rules! sol_attrs {
353        ($($id:ident : $e:expr),* $(,)?) => {
354            SolAttrs {
355                $($id: Some($e),)*
356                ..Default::default()
357            }
358        };
359    }
360
361    struct OuterAttribute(Vec<Attribute>);
362
363    impl syn::parse::Parse for OuterAttribute {
364        fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
365            input.call(Attribute::parse_outer).map(Self)
366        }
367    }
368
369    fn run_test(
370        attrs_s: &'static [&'static str],
371        expected: std::result::Result<SolAttrs, &'static str>,
372    ) {
373        let attrs: Vec<Attribute> =
374            attrs_s.iter().flat_map(|s| syn::parse_str::<OuterAttribute>(s).unwrap().0).collect();
375        match (SolAttrs::parse(&attrs), expected) {
376            (Ok((actual, _)), Ok(expected)) => assert_eq!(actual, expected, "{attrs_s:?}"),
377            (Err(actual), Err(expected)) => {
378                let actual = actual.to_string();
379                if !actual.contains(expected) {
380                    assert_eq!(actual, expected, "{attrs_s:?}")
381                }
382            }
383            (a, b) => panic!("assertion failed: `{a:?} != {b:?}`: {attrs_s:?}"),
384        }
385    }
386
387    test_sol_attrs! {
388        top_level {
389            #[cfg] => Ok(SolAttrs::default()),
390            #[cfg()] => Ok(SolAttrs::default()),
391            #[cfg = ""] => Ok(SolAttrs::default()),
392            #[derive()] #[sol()] => Ok(SolAttrs::default()),
393            #[sol()] => Ok(SolAttrs::default()),
394            #[sol()] #[sol()] => Ok(SolAttrs::default()),
395            #[sol = ""] => Err("expected `(`"),
396            #[sol] => Err("expected attribute arguments in parentheses: `sol(...)`"),
397
398            #[sol(() = "")] => Err("unexpected token in nested attribute, expected ident"),
399            #[sol(? = "")] => Err("unexpected token in nested attribute, expected ident"),
400            #[sol(::a)] => Err("expected ident"),
401            #[sol(::a = "")] => Err("expected ident"),
402            #[sol(a::b = "")] => Err("expected ident"),
403        }
404
405        extra {
406            #[sol(all_derives)] => Ok(sol_attrs! { all_derives: true }),
407            #[sol(all_derives = true)] => Ok(sol_attrs! { all_derives: true }),
408            #[sol(all_derives = false)] => Ok(sol_attrs! { all_derives: false }),
409            #[sol(all_derives = "false")] => Err("expected boolean literal"),
410            #[sol(all_derives)] #[sol(all_derives)] => Err(DUPLICATE_ERROR),
411
412            #[sol(extra_derives(Single, module::Double))] => Ok(sol_attrs! { extra_derives: vec![
413                parse_quote!(Single),
414                parse_quote!(module::Double),
415            ] }),
416
417            #[sol(extra_methods)] => Ok(sol_attrs! { extra_methods: true }),
418            #[sol(extra_methods = true)] => Ok(sol_attrs! { extra_methods: true }),
419            #[sol(extra_methods = false)] => Ok(sol_attrs! { extra_methods: false }),
420
421            #[sol(docs)] => Ok(sol_attrs! { docs: true }),
422            #[sol(docs = true)] => Ok(sol_attrs! { docs: true }),
423            #[sol(docs = false)] => Ok(sol_attrs! { docs: false }),
424
425            #[sol(abi)] => Ok(sol_attrs! { abi: true }),
426            #[sol(abi = true)] => Ok(sol_attrs! { abi: true }),
427            #[sol(abi = false)] => Ok(sol_attrs! { abi: false }),
428
429            #[sol(rpc)] => Ok(sol_attrs! { rpc: true }),
430            #[sol(rpc = true)] => Ok(sol_attrs! { rpc: true }),
431            #[sol(rpc = false)] => Ok(sol_attrs! { rpc: false }),
432
433            #[sol(alloy_sol_types)] => Err("expected `=`"),
434            #[sol(alloy_sol_types = alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy_core::sol_types) }),
435            #[sol(alloy_sol_types = ::alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy_core::sol_types) }),
436            #[sol(alloy_sol_types = alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy::sol_types) }),
437            #[sol(alloy_sol_types = ::alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy::sol_types) }),
438
439            #[sol(alloy_contract)] => Err("expected `=`"),
440            #[sol(alloy_contract = alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(alloy::contract) }),
441            #[sol(alloy_contract = ::alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(::alloy::contract) }),
442        }
443
444        rename {
445            #[sol(rename = "foo")] => Ok(sol_attrs! { rename: parse_quote!("foo") }),
446
447            #[sol(rename_all = "foo")] => Err("unsupported casing: foo"),
448            #[sol(rename_all = "camelcase")] => Ok(sol_attrs! { rename_all: CasingStyle::Camel }),
449            #[sol(rename_all = "camelCase")] #[sol(rename_all = "PascalCase")] => Err(DUPLICATE_ERROR),
450        }
451
452        bytecode {
453            #[sol(deployed_bytecode = "0x1234")] => Ok(sol_attrs! { deployed_bytecode: parse_quote!("0x1234") }),
454            #[sol(bytecode = "0x1234")] => Ok(sol_attrs! { bytecode: parse_quote!("0x1234") }),
455            #[sol(bytecode = "1234")] => Ok(sol_attrs! { bytecode: parse_quote!("1234") }),
456            #[sol(bytecode = "0x123xyz")] => Err("invalid hex value: "),
457            #[sol(bytecode = "12 34")] => Err("invalid hex value: "),
458            #[sol(bytecode = "xyz")] => Err("invalid hex value: "),
459            #[sol(bytecode = "123")] => Err("invalid hex value: "),
460        }
461
462        type_check {
463            #[sol(type_check = "my_function")] => Ok(sol_attrs! { type_check: parse_quote!("my_function") }),
464            #[sol(type_check = "my_function1")] #[sol(type_check = "my_function2")] => Err(DUPLICATE_ERROR),
465        }
466
467        #[cfg_attr(miri, ignore = "env not available")]
468        inner_macro {
469            #[sol(rename = env!("CARGO_PKG_NAME"))] => Ok(sol_attrs! { rename: parse_quote!("alloy-sol-macro-input") }),
470        }
471    }
472}