Skip to main content

codama_attributes/
attributes.rs

1use crate::{
2    Attribute, AttributeContext, CodamaAttribute, CodamaDirective, DeriveAttribute,
3    ExportDirective, TryFromFilter,
4};
5use codama_errors::IteratorCombineErrors;
6use codama_syn_helpers::extensions::*;
7use std::ops::{Deref, DerefMut, Index, IndexMut};
8
9#[derive(Debug, PartialEq)]
10pub struct Attributes<'a>(pub Vec<Attribute<'a>>);
11
12impl<'a> Attributes<'a> {
13    pub fn parse(attrs: &'a [syn::Attribute], ctx: AttributeContext<'a>) -> syn::Result<Self> {
14        let attributes = Self(
15            attrs
16                .iter()
17                // Expand multi-attr cfg_attr into (ast, effective) pairs
18                .flat_map(|ast| {
19                    let inners = ast.unfeatured_all();
20                    if inners.len() <= 1 {
21                        // Not a multi-attr cfg_attr - use standard parsing
22                        let unfeatured = ast.unfeatured();
23                        let effective = unfeatured.unwrap_or_else(|| (*ast).clone());
24                        vec![(ast, effective)]
25                    } else {
26                        // Multi-attr cfg_attr - expand each inner attribute
27                        inners.into_iter().map(|inner| (ast, inner)).collect()
28                    }
29                })
30                .map(|(ast, effective)| Attribute::parse_from(ast, &effective, &ctx))
31                .collect_and_combine_errors()?,
32        );
33        attributes.validate_codama_type_attributes()?;
34        Ok(attributes)
35    }
36
37    pub fn validate_codama_type_attributes(&self) -> syn::Result<()> {
38        let mut errors = Vec::<syn::Error>::new();
39        let mut has_seen_type = false;
40
41        for attribute in self.0.iter().rev() {
42            if let Attribute::Codama(attribute) = attribute {
43                match attribute.directive.as_ref() {
44                    CodamaDirective::Type(_) if !has_seen_type => has_seen_type = true,
45                    CodamaDirective::Type(_)
46                    | CodamaDirective::Encoding(_)
47                    | CodamaDirective::FixedSize(_)
48                        if has_seen_type =>
49                    {
50                        errors.push(syn::Error::new_spanned(
51                            attribute.ast,
52                            "This attribute is overridden by a `#[codama(type = ...)]` attribute below",
53                        ));
54                    }
55                    _ => {}
56                }
57            }
58        }
59
60        if errors.is_empty() {
61            Ok(())
62        } else {
63            // Combine all errors into one
64            let mut combined_error = errors.remove(0);
65            for error in errors {
66                combined_error.combine(error);
67            }
68            Err(combined_error)
69        }
70    }
71
72    pub fn has_any_codama_derive(&self) -> bool {
73        self.has_codama_derive("CodamaAccount")
74            || self.has_codama_derive("CodamaAccounts")
75            || self.has_codama_derive("CodamaErrors")
76            || self.has_codama_derive("CodamaEvent")
77            || self.has_codama_derive("CodamaEvents")
78            || self.has_codama_derive("CodamaInstruction")
79            || self.has_codama_derive("CodamaInstructions")
80            || self.has_codama_derive("CodamaPda")
81            || self.has_codama_derive("CodamaType")
82    }
83
84    pub fn has_codama_derive(&self, derive: &str) -> bool {
85        self.has_derive(&["", "codama", "codama_macros"], derive)
86    }
87
88    pub fn has_derive(&self, prefixes: &[&str], last: &str) -> bool {
89        self.iter().filter_map(DeriveAttribute::filter).any(|attr| {
90            attr.derives
91                .iter()
92                .any(|p| p.last_str() == last && prefixes.contains(&p.prefix().as_str()))
93        })
94    }
95
96    pub fn has_codama_export(&self, derive: &str) -> bool {
97        self.iter()
98            .filter_map(ExportDirective::filter)
99            .any(|e| e.derive == derive)
100    }
101
102    pub fn has_codama_attribute(&self, name: &str) -> bool {
103        self.iter()
104            .filter_map(CodamaAttribute::filter)
105            .any(|a| a.directive.name() == name)
106    }
107
108    pub fn get_all<B: 'a, F>(&'a self, f: F) -> Vec<&'a B>
109    where
110        F: Fn(&'a Attribute<'a>) -> Option<&'a B>,
111    {
112        self.iter().filter_map(f).collect()
113    }
114
115    pub fn get_first<B: 'a, F>(&'a self, f: F) -> Option<&'a B>
116    where
117        F: Fn(&'a Attribute<'a>) -> Option<&'a B>,
118    {
119        self.iter().find_map(f)
120    }
121
122    pub fn get_last<B: 'a, F>(&'a self, f: F) -> Option<&'a B>
123    where
124        F: Fn(&'a Attribute<'a>) -> Option<&'a B>,
125    {
126        self.iter().rev().find_map(f)
127    }
128}
129
130impl<'a> Deref for Attributes<'a> {
131    type Target = Vec<Attribute<'a>>;
132
133    fn deref(&self) -> &Self::Target {
134        &self.0
135    }
136}
137
138impl DerefMut for Attributes<'_> {
139    fn deref_mut(&mut self) -> &mut Self::Target {
140        &mut self.0
141    }
142}
143
144impl<'a> AsRef<[Attribute<'a>]> for Attributes<'a> {
145    fn as_ref(&self) -> &[Attribute<'a>] {
146        &self.0
147    }
148}
149
150impl<'a> AsMut<[Attribute<'a>]> for Attributes<'a> {
151    fn as_mut(&mut self) -> &mut [Attribute<'a>] {
152        &mut self.0
153    }
154}
155
156impl<'a> Index<usize> for Attributes<'a> {
157    type Output = Attribute<'a>;
158
159    fn index(&self, index: usize) -> &Self::Output {
160        &self.0[index]
161    }
162}
163
164impl IndexMut<usize> for Attributes<'_> {
165    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
166        &mut self.0[index]
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use syn::parse_quote;
174
175    fn file_ctx() -> syn::File {
176        syn::File::empty()
177    }
178
179    #[test]
180    fn parse_single_codama_attr() {
181        let file = file_ctx();
182        let attrs: Vec<syn::Attribute> = vec![parse_quote! { #[codama(type = boolean)] }];
183        let ctx = AttributeContext::File(&file);
184        let attributes = Attributes::parse(&attrs, ctx).unwrap();
185
186        assert_eq!(attributes.len(), 1);
187        assert!(matches!(&attributes[0], Attribute::Codama(_)));
188    }
189
190    #[test]
191    fn parse_feature_gated_single_codama_attr() {
192        let file = file_ctx();
193        let attrs: Vec<syn::Attribute> =
194            vec![parse_quote! { #[cfg_attr(feature = "codama", codama(type = boolean))] }];
195        let ctx = AttributeContext::File(&file);
196        let attributes = Attributes::parse(&attrs, ctx).unwrap();
197
198        assert_eq!(attributes.len(), 1);
199        assert!(matches!(&attributes[0], Attribute::Codama(_)));
200    }
201
202    #[test]
203    fn parse_multi_attr_cfg_attr_expands_all() {
204        let file = file_ctx();
205        // This is the bug scenario: multi-attr cfg_attr should expand to multiple attributes
206        let attrs: Vec<syn::Attribute> = vec![parse_quote! {
207            #[cfg_attr(feature = "codama", codama(type = boolean), codama(name = "foo"))]
208        }];
209        let ctx = AttributeContext::File(&file);
210        let attributes = Attributes::parse(&attrs, ctx).unwrap();
211
212        // Should have 2 attributes from the expansion
213        assert_eq!(attributes.len(), 2);
214        assert!(matches!(&attributes[0], Attribute::Codama(_)));
215        assert!(matches!(&attributes[1], Attribute::Codama(_)));
216
217        // Verify the directives
218        if let Attribute::Codama(attr) = &attributes[0] {
219            assert!(matches!(attr.directive.as_ref(), CodamaDirective::Type(_)));
220        }
221        if let Attribute::Codama(attr) = &attributes[1] {
222            assert!(matches!(attr.directive.as_ref(), CodamaDirective::Name(_)));
223        }
224    }
225
226    #[test]
227    fn parse_multi_attr_cfg_attr_mixed_types() {
228        let file = file_ctx();
229        // cfg_attr with mixed attribute types: derive, codama
230        let attrs: Vec<syn::Attribute> = vec![parse_quote! {
231            #[cfg_attr(feature = "x", derive(Debug), codama(type = boolean))]
232        }];
233        let ctx = AttributeContext::File(&file);
234        let attributes = Attributes::parse(&attrs, ctx).unwrap();
235
236        // Should have 2 attributes: Derive and Codama
237        assert_eq!(attributes.len(), 2);
238        assert!(matches!(&attributes[0], Attribute::Derive(_)));
239        assert!(matches!(&attributes[1], Attribute::Codama(_)));
240    }
241
242    #[test]
243    fn parse_multi_attr_cfg_attr_preserves_order() {
244        let file = file_ctx();
245        let attrs: Vec<syn::Attribute> = vec![parse_quote! {
246            #[cfg_attr(feature = "codama", codama(name = "first"), codama(name = "second"), codama(name = "third"))]
247        }];
248        let ctx = AttributeContext::File(&file);
249        let attributes = Attributes::parse(&attrs, ctx).unwrap();
250
251        assert_eq!(attributes.len(), 3);
252
253        // All should be Name directives in order
254        let names: Vec<_> = attributes
255            .iter()
256            .filter_map(CodamaAttribute::filter)
257            .filter_map(|a| {
258                if let CodamaDirective::Name(n) = a.directive.as_ref() {
259                    Some(n.name.as_ref().to_string())
260                } else {
261                    None
262                }
263            })
264            .collect();
265        assert_eq!(names, vec!["first", "second", "third"]);
266    }
267
268    #[test]
269    fn parse_multiple_separate_cfg_attr_and_multi_attr() {
270        let file = file_ctx();
271        // Mix of separate attrs and multi-attr cfg_attr
272        let attrs: Vec<syn::Attribute> = vec![
273            parse_quote! { #[derive(Clone)] },
274            parse_quote! { #[cfg_attr(feature = "x", codama(name = "a"), codama(name = "b"))] },
275            parse_quote! { #[codama(type = boolean)] },
276        ];
277        let ctx = AttributeContext::File(&file);
278        let attributes = Attributes::parse(&attrs, ctx).unwrap();
279
280        // Should have 4 attributes: Derive, 2 Codama from cfg_attr, 1 Codama bare
281        assert_eq!(attributes.len(), 4);
282        assert!(matches!(&attributes[0], Attribute::Derive(_)));
283        assert!(matches!(&attributes[1], Attribute::Codama(_)));
284        assert!(matches!(&attributes[2], Attribute::Codama(_)));
285        assert!(matches!(&attributes[3], Attribute::Codama(_)));
286    }
287}