Skip to main content

codama_attributes/
attributes.rs

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