Skip to main content

codama_syn_helpers/extensions/
attribute.rs

1use super::PathExtension;
2use syn::{punctuated::Punctuated, Attribute};
3
4pub trait AttributeExtension {
5    fn get_self(&self) -> &Attribute;
6
7    /// Parse all arguments as comma-separated types.
8    fn parse_comma_args<T: syn::parse::Parse>(&self) -> syn::Result<Vec<T>> {
9        self.get_self()
10            .parse_args_with(Punctuated::<T, syn::Token![,]>::parse_terminated)
11            .map(|metas| metas.into_iter().collect::<Vec<_>>())
12    }
13
14    /// Unwrap all inner attributes from a feature-gated `cfg_attr`.
15    /// E.g. `#[cfg_attr(feature = "x", derive(Debug), codama(...))]`
16    /// returns `vec![#[derive(Debug)], #[codama(...)]]`.
17    /// Returns an empty Vec if not a feature-gated `cfg_attr`.
18    fn unfeatured_all(&self) -> Vec<Attribute> {
19        let this = self.get_self();
20        if !this.path().is_strict("cfg_attr") {
21            return vec![];
22        }
23        let metas = match this.parse_comma_args::<syn::Meta>() {
24            Ok(m) => m,
25            Err(_) => return vec![],
26        };
27        if metas.len() < 2 {
28            return vec![];
29        }
30        // First item should be the feature condition
31        match &metas[0] {
32            syn::Meta::NameValue(m) if m.path.is_strict("feature") => (),
33            _ => return vec![],
34        }
35        // Rest are the inner attributes
36        metas[1..]
37            .iter()
38            .map(|meta| Attribute {
39                pound_token: this.pound_token,
40                style: this.style,
41                bracket_token: this.bracket_token,
42                meta: meta.clone(),
43            })
44            .collect()
45    }
46
47    /// Unwrap the feature flag from the attribute.
48    /// E.g. `#[cfg_attr(feature = "some_feature", derive(Debug))]`
49    /// becomes the syn::Attribute defined as `#[derive(Debug)]`.
50    /// If the `cfg_attr` contains multiple inner attributes, returns the first.
51    fn unfeatured(&self) -> Option<Attribute> {
52        self.unfeatured_all().into_iter().next()
53    }
54}
55
56impl AttributeExtension for Attribute {
57    fn get_self(&self) -> &Attribute {
58        self
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use quote::{quote, ToTokens};
66    use syn::parse_quote;
67
68    #[test]
69    fn parse_comma_args_ok() {
70        let attribute: Attribute = parse_quote! { #[foo(42, "bar")] };
71        let args = attribute.parse_comma_args::<syn::Lit>().unwrap();
72        assert_eq!(args.len(), 2);
73    }
74
75    #[test]
76    fn parse_comma_args_err() {
77        let attribute: Attribute = parse_quote! { #[foo] };
78        let args = attribute.parse_comma_args::<syn::Path>();
79        assert!(args.is_err());
80    }
81
82    #[test]
83    fn unfeatured_single() {
84        let attribute: Attribute =
85            parse_quote! { #[cfg_attr(feature = "some_feature", derive(Debug))] };
86        let unfeatured = attribute.unfeatured();
87        assert_eq!(
88            unfeatured.to_token_stream().to_string(),
89            quote! { #[derive(Debug)] }.to_string()
90        );
91    }
92
93    #[test]
94    fn unfeatured_multiple_returns_first() {
95        let attribute: Attribute =
96            parse_quote! { #[cfg_attr(feature = "x", derive(Debug), codama(foo))] };
97        let unfeatured = attribute.unfeatured();
98        assert_eq!(
99            unfeatured.to_token_stream().to_string(),
100            quote! { #[derive(Debug)] }.to_string()
101        );
102    }
103
104    #[test]
105    fn unfeatured_not_cfg_attr() {
106        let attribute: Attribute = parse_quote! { #[derive(Debug)] };
107        let unfeatured = attribute.unfeatured();
108        assert_eq!(unfeatured, None);
109    }
110
111    #[test]
112    fn unfeatured_all_single() {
113        let attribute: Attribute = parse_quote! { #[cfg_attr(feature = "x", derive(Debug))] };
114        let all = attribute.unfeatured_all();
115        assert_eq!(all.len(), 1);
116        assert_eq!(
117            all[0].to_token_stream().to_string(),
118            quote! { #[derive(Debug)] }.to_string()
119        );
120    }
121
122    #[test]
123    fn unfeatured_all_multiple() {
124        let attribute: Attribute = parse_quote! {
125            #[cfg_attr(feature = "codama", codama(account(name = "stake")), codama(account(name = "auth")))]
126        };
127        let all = attribute.unfeatured_all();
128        assert_eq!(all.len(), 2);
129        assert_eq!(
130            all[0].to_token_stream().to_string(),
131            quote! { #[codama(account(name = "stake"))] }.to_string()
132        );
133        assert_eq!(
134            all[1].to_token_stream().to_string(),
135            quote! { #[codama(account(name = "auth"))] }.to_string()
136        );
137    }
138
139    #[test]
140    fn unfeatured_all_not_cfg_attr() {
141        let attribute: Attribute = parse_quote! { #[derive(Debug)] };
142        let all = attribute.unfeatured_all();
143        assert!(all.is_empty());
144    }
145
146    #[test]
147    fn unfeatured_all_mixed_attrs() {
148        let attribute: Attribute = parse_quote! {
149            #[cfg_attr(feature = "x", derive(Debug), repr(u8), codama(foo))]
150        };
151        let all = attribute.unfeatured_all();
152        assert_eq!(all.len(), 3);
153        assert_eq!(
154            all[0].to_token_stream().to_string(),
155            quote! { #[derive(Debug)] }.to_string()
156        );
157        assert_eq!(
158            all[1].to_token_stream().to_string(),
159            quote! { #[repr(u8)] }.to_string()
160        );
161        assert_eq!(
162            all[2].to_token_stream().to_string(),
163            quote! { #[codama(foo)] }.to_string()
164        );
165    }
166}