Skip to main content

alloy_sol_macro_input/
attr.rs

1use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
2use proc_macro2::TokenStream;
3use quote::quote;
4use syn::{
5    Attribute, Error, LitBool, LitStr, Path, Result, Token, meta::ParseNestedMeta, parse::Parse,
6    punctuated::Punctuated,
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(Clone, 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    /// Ignore unlinked bytecode
115    /// `#[sol(ignore_unlinked)]`
116    pub ignore_unlinked: Option<bool>,
117}
118
119impl SolAttrs {
120    /// Parse the `#[sol(...)]` attributes from a list of attributes.
121    pub fn parse(attrs: &[Attribute]) -> Result<(Self, Vec<Attribute>)> {
122        let mut this = Self::default();
123        let mut others = Vec::with_capacity(attrs.len());
124        for attr in attrs {
125            if !attr.path().is_ident("sol") {
126                others.push(attr.clone());
127                continue;
128            }
129
130            attr.meta.require_list()?.parse_nested_meta(|meta| {
131                let path = meta.path.get_ident().ok_or_else(|| meta.error("expected ident"))?;
132                let s = path.to_string();
133
134                macro_rules! match_ {
135                    ($($l:ident => $e:expr),* $(,)?) => {
136                        match s.as_str() {
137                            $(
138                                stringify!($l) => if this.$l.is_some() {
139                                    return Err(meta.error(DUPLICATE_ERROR))
140                                } else {
141                                    this.$l = Some($e);
142                                },
143                            )*
144                            _ => return Err(meta.error(UNKNOWN_ERROR)),
145                        }
146                    };
147                }
148
149                // `path` => true, `path = <bool>` => <bool>
150                let bool = || {
151                    if let Ok(input) = meta.value() {
152                        input.parse::<LitBool>().map(|lit| lit.value)
153                    } else {
154                        Ok(true)
155                    }
156                };
157
158                // `path = <path>`
159                let path = || meta.value()?.parse::<Path>();
160
161                // `path = "<str>"`
162                let lit = || {
163                    let value = meta.value()?;
164                    let span = value.span();
165                    let macro_string::MacroString(value) =
166                        value.parse::<macro_string::MacroString>()?;
167                    Ok::<_, syn::Error>(LitStr::new(&value, span))
168                };
169
170                // `path = "0x<hex>"`
171                let bytes = || {
172                    let lit = lit()?;
173                    if let Err(e) = hex::check(lit.value()) {
174                        let msg = format!("invalid hex value: {e}");
175                        return Err(Error::new(lit.span(), msg));
176                    }
177                    Ok(lit)
178                };
179
180                // `path(comma, separated, list)`
181                fn list<T>(
182                    meta: &ParseNestedMeta<'_>,
183                    parser: fn(syn::parse::ParseStream<'_>) -> Result<T>,
184                ) -> Result<Vec<T>> {
185                    let content;
186                    syn::parenthesized!(content in meta.input);
187                    Ok(content.parse_terminated(parser, Token![,])?.into_iter().collect())
188                }
189
190                match_! {
191                    rpc => bool()?,
192                    abi => bool()?,
193                    all_derives => bool()?,
194                    extra_derives => list(&meta, Path::parse)?,
195                    extra_methods => bool()?,
196                    docs => bool()?,
197
198                    alloy_sol_types => path()?,
199                    alloy_contract => path()?,
200
201                    rename => lit()?,
202                    rename_all => CasingStyle::from_lit(&lit()?)?,
203
204                    bytecode => bytes()?,
205                    deployed_bytecode => bytes()?,
206
207                    type_check => lit()?,
208                    ignore_unlinked => bool()?,
209                };
210                Ok(())
211            })?;
212        }
213        Ok((this, others))
214    }
215
216    /// Merges `other` into `self`. `other` takes precedence over `self`.
217    ///
218    /// This is used to inherit the contract's attributes when expanding its items.
219    pub fn merge(&mut self, other: &SolAttrs) {
220        fn merge_opt<T: Clone>(a: &mut Option<T>, b: &Option<T>) {
221            if let Some(b) = b {
222                *a = Some(b.clone());
223            }
224        }
225        fn merge_vec<T: Clone>(a: &mut Option<Vec<T>>, b: &Option<Vec<T>>) {
226            if let Some(b) = b {
227                a.get_or_insert_default().extend(b.iter().cloned());
228            }
229        }
230        let (a, b) = (self, other);
231        merge_opt(&mut a.rpc, &b.rpc);
232        merge_opt(&mut a.abi, &b.abi);
233        merge_opt(&mut a.all_derives, &b.all_derives);
234        merge_vec(&mut a.extra_derives, &b.extra_derives);
235        merge_opt(&mut a.extra_methods, &b.extra_methods);
236        merge_opt(&mut a.docs, &b.docs);
237        merge_opt(&mut a.alloy_sol_types, &b.alloy_sol_types);
238        merge_opt(&mut a.alloy_contract, &b.alloy_contract);
239        merge_opt(&mut a.rename, &b.rename);
240        merge_opt(&mut a.rename_all, &b.rename_all);
241        merge_opt(&mut a.bytecode, &b.bytecode);
242        merge_opt(&mut a.deployed_bytecode, &b.deployed_bytecode);
243        merge_opt(&mut a.type_check, &b.type_check);
244        merge_opt(&mut a.ignore_unlinked, &b.ignore_unlinked);
245    }
246}
247
248/// Trait for items that contain `#[sol(...)]` attributes among other
249/// attributes. This is usually a shortcut  for [`SolAttrs::parse`].
250pub trait ContainsSolAttrs {
251    /// Get the list of attributes.
252    fn attrs(&self) -> &[Attribute];
253
254    /// Parse the `#[sol(...)]` attributes from the list of attributes.
255    fn split_attrs(&self) -> syn::Result<(SolAttrs, Vec<Attribute>)> {
256        SolAttrs::parse(self.attrs())
257    }
258}
259
260impl ContainsSolAttrs for syn_solidity::File {
261    fn attrs(&self) -> &[Attribute] {
262        &self.attrs
263    }
264}
265
266impl ContainsSolAttrs for syn_solidity::ItemContract {
267    fn attrs(&self) -> &[Attribute] {
268        &self.attrs
269    }
270}
271
272impl ContainsSolAttrs for syn_solidity::ItemEnum {
273    fn attrs(&self) -> &[Attribute] {
274        &self.attrs
275    }
276}
277
278impl ContainsSolAttrs for syn_solidity::ItemError {
279    fn attrs(&self) -> &[Attribute] {
280        &self.attrs
281    }
282}
283
284impl ContainsSolAttrs for syn_solidity::ItemEvent {
285    fn attrs(&self) -> &[Attribute] {
286        &self.attrs
287    }
288}
289
290impl ContainsSolAttrs for syn_solidity::ItemFunction {
291    fn attrs(&self) -> &[Attribute] {
292        &self.attrs
293    }
294}
295
296impl ContainsSolAttrs for syn_solidity::ItemStruct {
297    fn attrs(&self) -> &[Attribute] {
298        &self.attrs
299    }
300}
301
302impl ContainsSolAttrs for syn_solidity::ItemUdt {
303    fn attrs(&self) -> &[Attribute] {
304        &self.attrs
305    }
306}
307
308/// Defines the casing for the attributes long representation.
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum CasingStyle {
311    /// Indicate word boundaries with uppercase letter, excluding the first
312    /// word.
313    Camel,
314    /// Keep all letters lowercase and indicate word boundaries with hyphens.
315    Kebab,
316    /// Indicate word boundaries with uppercase letter, including the first
317    /// word.
318    Pascal,
319    /// Keep all letters uppercase and indicate word boundaries with
320    /// underscores.
321    ScreamingSnake,
322    /// Keep all letters lowercase and indicate word boundaries with
323    /// underscores.
324    Snake,
325    /// Keep all letters lowercase and remove word boundaries.
326    Lower,
327    /// Keep all letters uppercase and remove word boundaries.
328    Upper,
329    /// Use the original attribute name defined in the code.
330    Verbatim,
331}
332
333impl CasingStyle {
334    fn from_lit(name: &LitStr) -> Result<Self> {
335        let normalized = name.value().to_upper_camel_case().to_lowercase();
336        let s = match normalized.as_ref() {
337            "camel" | "camelcase" => Self::Camel,
338            "kebab" | "kebabcase" => Self::Kebab,
339            "pascal" | "pascalcase" => Self::Pascal,
340            "screamingsnake" | "screamingsnakecase" => Self::ScreamingSnake,
341            "snake" | "snakecase" => Self::Snake,
342            "lower" | "lowercase" => Self::Lower,
343            "upper" | "uppercase" => Self::Upper,
344            "verbatim" | "verbatimcase" => Self::Verbatim,
345            s => return Err(Error::new(name.span(), format!("unsupported casing: {s}"))),
346        };
347        Ok(s)
348    }
349
350    /// Apply the casing style to the given string.
351    #[allow(dead_code)]
352    pub fn apply(self, s: &str) -> String {
353        match self {
354            Self::Pascal => s.to_upper_camel_case(),
355            Self::Kebab => s.to_kebab_case(),
356            Self::Camel => s.to_lower_camel_case(),
357            Self::ScreamingSnake => s.to_shouty_snake_case(),
358            Self::Snake => s.to_snake_case(),
359            Self::Lower => s.to_snake_case().replace('_', ""),
360            Self::Upper => s.to_shouty_snake_case().replace('_', ""),
361            Self::Verbatim => s.to_owned(),
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use syn::parse_quote;
370
371    macro_rules! test_sol_attrs {
372        ($($(#[$attr:meta])* $group:ident { $($t:tt)* })+) => {$(
373            #[test]
374            $(#[$attr])*
375            fn $group() {
376                test_sol_attrs! { $($t)* }
377            }
378        )+};
379
380        ($( $(#[$attr:meta])* => $expected:expr ),+ $(,)?) => {$(
381            run_test(
382                &[$(stringify!(#[$attr])),*],
383                $expected
384            );
385        )+};
386    }
387
388    macro_rules! sol_attrs {
389        ($($id:ident : $e:expr),* $(,)?) => {
390            SolAttrs {
391                $($id: Some($e),)*
392                ..Default::default()
393            }
394        };
395    }
396
397    struct OuterAttribute(Vec<Attribute>);
398
399    impl syn::parse::Parse for OuterAttribute {
400        fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
401            input.call(Attribute::parse_outer).map(Self)
402        }
403    }
404
405    fn run_test(
406        attrs_s: &'static [&'static str],
407        expected: std::result::Result<SolAttrs, &'static str>,
408    ) {
409        let attrs: Vec<Attribute> =
410            attrs_s.iter().flat_map(|s| syn::parse_str::<OuterAttribute>(s).unwrap().0).collect();
411        match (SolAttrs::parse(&attrs), expected) {
412            (Ok((actual, _)), Ok(expected)) => assert_eq!(actual, expected, "{attrs_s:?}"),
413            (Err(actual), Err(expected)) => {
414                let actual = actual.to_string();
415                if !actual.contains(expected) {
416                    assert_eq!(actual, expected, "{attrs_s:?}")
417                }
418            }
419            (a, b) => panic!("assertion failed: `{a:?} != {b:?}`: {attrs_s:?}"),
420        }
421    }
422
423    test_sol_attrs! {
424        top_level {
425            #[cfg] => Ok(SolAttrs::default()),
426            #[cfg()] => Ok(SolAttrs::default()),
427            #[cfg = ""] => Ok(SolAttrs::default()),
428            #[derive()] #[sol()] => Ok(SolAttrs::default()),
429            #[sol()] => Ok(SolAttrs::default()),
430            #[sol()] #[sol()] => Ok(SolAttrs::default()),
431            #[sol = ""] => Err("expected `(`"),
432            #[sol] => Err("expected attribute arguments in parentheses: `sol(...)`"),
433
434            #[sol(() = "")] => Err("unexpected token in nested attribute, expected ident"),
435            #[sol(? = "")] => Err("unexpected token in nested attribute, expected ident"),
436            #[sol(::a)] => Err("expected ident"),
437            #[sol(::a = "")] => Err("expected ident"),
438            #[sol(a::b = "")] => Err("expected ident"),
439        }
440
441        extra {
442            #[sol(all_derives)] => Ok(sol_attrs! { all_derives: true }),
443            #[sol(all_derives = true)] => Ok(sol_attrs! { all_derives: true }),
444            #[sol(all_derives = false)] => Ok(sol_attrs! { all_derives: false }),
445            #[sol(all_derives = "false")] => Err("expected boolean literal"),
446            #[sol(all_derives)] #[sol(all_derives)] => Err(DUPLICATE_ERROR),
447
448            #[sol(extra_derives(Single, module::Double))] => Ok(sol_attrs! { extra_derives: vec![
449                parse_quote!(Single),
450                parse_quote!(module::Double),
451            ] }),
452
453            #[sol(extra_methods)] => Ok(sol_attrs! { extra_methods: true }),
454            #[sol(extra_methods = true)] => Ok(sol_attrs! { extra_methods: true }),
455            #[sol(extra_methods = false)] => Ok(sol_attrs! { extra_methods: false }),
456
457            #[sol(docs)] => Ok(sol_attrs! { docs: true }),
458            #[sol(docs = true)] => Ok(sol_attrs! { docs: true }),
459            #[sol(docs = false)] => Ok(sol_attrs! { docs: false }),
460
461            #[sol(abi)] => Ok(sol_attrs! { abi: true }),
462            #[sol(abi = true)] => Ok(sol_attrs! { abi: true }),
463            #[sol(abi = false)] => Ok(sol_attrs! { abi: false }),
464
465            #[sol(rpc)] => Ok(sol_attrs! { rpc: true }),
466            #[sol(rpc = true)] => Ok(sol_attrs! { rpc: true }),
467            #[sol(rpc = false)] => Ok(sol_attrs! { rpc: false }),
468
469            #[sol(alloy_sol_types)] => Err("expected `=`"),
470            #[sol(alloy_sol_types = alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy_core::sol_types) }),
471            #[sol(alloy_sol_types = ::alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy_core::sol_types) }),
472            #[sol(alloy_sol_types = alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy::sol_types) }),
473            #[sol(alloy_sol_types = ::alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy::sol_types) }),
474
475            #[sol(alloy_contract)] => Err("expected `=`"),
476            #[sol(alloy_contract = alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(alloy::contract) }),
477            #[sol(alloy_contract = ::alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(::alloy::contract) }),
478        }
479
480        rename {
481            #[sol(rename = "foo")] => Ok(sol_attrs! { rename: parse_quote!("foo") }),
482
483            #[sol(rename_all = "foo")] => Err("unsupported casing: foo"),
484            #[sol(rename_all = "camelcase")] => Ok(sol_attrs! { rename_all: CasingStyle::Camel }),
485            #[sol(rename_all = "camelCase")] #[sol(rename_all = "PascalCase")] => Err(DUPLICATE_ERROR),
486        }
487
488        bytecode {
489            #[sol(deployed_bytecode = "0x1234")] => Ok(sol_attrs! { deployed_bytecode: parse_quote!("0x1234") }),
490            #[sol(bytecode = "0x1234")] => Ok(sol_attrs! { bytecode: parse_quote!("0x1234") }),
491            #[sol(bytecode = "1234")] => Ok(sol_attrs! { bytecode: parse_quote!("1234") }),
492            #[sol(bytecode = "0x123xyz")] => Err("invalid hex value: "),
493            #[sol(bytecode = "12 34")] => Err("invalid hex value: "),
494            #[sol(bytecode = "xyz")] => Err("invalid hex value: "),
495            #[sol(bytecode = "123")] => Err("invalid hex value: "),
496        }
497
498        type_check {
499            #[sol(type_check = "my_function")] => Ok(sol_attrs! { type_check: parse_quote!("my_function") }),
500            #[sol(type_check = "my_function1")] #[sol(type_check = "my_function2")] => Err(DUPLICATE_ERROR),
501        }
502
503        #[cfg_attr(miri, ignore = "env not available")]
504        inner_macro {
505            #[sol(rename = env!("CARGO_PKG_NAME"))] => Ok(sol_attrs! { rename: parse_quote!("alloy-sol-macro-input") }),
506        }
507
508        ignore_unlinked {
509            #[sol(ignore_unlinked)] => Ok(sol_attrs! { ignore_unlinked: true }),
510            #[sol(ignore_unlinked = true)] => Ok(sol_attrs! { ignore_unlinked: true }),
511            #[sol(ignore_unlinked = false)] => Ok(sol_attrs! { ignore_unlinked: false }),
512        }
513    }
514}