codama_attributes/
attributes.rs1use 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 .flat_map(|ast| {
18 let inners = ast.unfeatured_all();
19 if inners.len() <= 1 {
20 let unfeatured = ast.unfeatured();
22 let effective = unfeatured.unwrap_or_else(|| (*ast).clone());
23 vec![(ast, effective)]
24 } else {
25 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 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("CodamaInstruction")
76 || self.has_codama_derive("CodamaInstructions")
77 || self.has_codama_derive("CodamaPda")
78 || self.has_codama_derive("CodamaType")
79 }
80
81 pub fn has_codama_derive(&self, derive: &str) -> bool {
82 self.has_derive(&["", "codama", "codama_macros"], derive)
83 }
84
85 pub fn has_derive(&self, prefixes: &[&str], last: &str) -> bool {
86 self.iter().filter_map(DeriveAttribute::filter).any(|attr| {
87 attr.derives
88 .iter()
89 .any(|p| p.last_str() == last && prefixes.contains(&p.prefix().as_str()))
90 })
91 }
92
93 pub fn has_codama_attribute(&self, name: &str) -> bool {
94 self.iter()
95 .filter_map(CodamaAttribute::filter)
96 .any(|a| a.directive.name() == name)
97 }
98
99 pub fn get_all<B: 'a, F>(&'a self, f: F) -> Vec<&'a B>
100 where
101 F: Fn(&'a Attribute<'a>) -> Option<&'a B>,
102 {
103 self.iter().filter_map(f).collect()
104 }
105
106 pub fn get_first<B: 'a, F>(&'a self, f: F) -> Option<&'a B>
107 where
108 F: Fn(&'a Attribute<'a>) -> Option<&'a B>,
109 {
110 self.iter().find_map(f)
111 }
112
113 pub fn get_last<B: 'a, F>(&'a self, f: F) -> Option<&'a B>
114 where
115 F: Fn(&'a Attribute<'a>) -> Option<&'a B>,
116 {
117 self.iter().rev().find_map(f)
118 }
119}
120
121impl<'a> Deref for Attributes<'a> {
122 type Target = Vec<Attribute<'a>>;
123
124 fn deref(&self) -> &Self::Target {
125 &self.0
126 }
127}
128
129impl DerefMut for Attributes<'_> {
130 fn deref_mut(&mut self) -> &mut Self::Target {
131 &mut self.0
132 }
133}
134
135impl<'a> AsRef<[Attribute<'a>]> for Attributes<'a> {
136 fn as_ref(&self) -> &[Attribute<'a>] {
137 &self.0
138 }
139}
140
141impl<'a> AsMut<[Attribute<'a>]> for Attributes<'a> {
142 fn as_mut(&mut self) -> &mut [Attribute<'a>] {
143 &mut self.0
144 }
145}
146
147impl<'a> Index<usize> for Attributes<'a> {
148 type Output = Attribute<'a>;
149
150 fn index(&self, index: usize) -> &Self::Output {
151 &self.0[index]
152 }
153}
154
155impl IndexMut<usize> for Attributes<'_> {
156 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
157 &mut self.0[index]
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use syn::parse_quote;
165
166 fn file_ctx() -> syn::File {
167 syn::File::empty()
168 }
169
170 #[test]
171 fn parse_single_codama_attr() {
172 let file = file_ctx();
173 let attrs: Vec<syn::Attribute> = vec![parse_quote! { #[codama(type = boolean)] }];
174 let ctx = AttributeContext::File(&file);
175 let attributes = Attributes::parse(&attrs, ctx).unwrap();
176
177 assert_eq!(attributes.len(), 1);
178 assert!(matches!(&attributes[0], Attribute::Codama(_)));
179 }
180
181 #[test]
182 fn parse_feature_gated_single_codama_attr() {
183 let file = file_ctx();
184 let attrs: Vec<syn::Attribute> =
185 vec![parse_quote! { #[cfg_attr(feature = "codama", codama(type = boolean))] }];
186 let ctx = AttributeContext::File(&file);
187 let attributes = Attributes::parse(&attrs, ctx).unwrap();
188
189 assert_eq!(attributes.len(), 1);
190 assert!(matches!(&attributes[0], Attribute::Codama(_)));
191 }
192
193 #[test]
194 fn parse_multi_attr_cfg_attr_expands_all() {
195 let file = file_ctx();
196 let attrs: Vec<syn::Attribute> = vec![parse_quote! {
198 #[cfg_attr(feature = "codama", codama(type = boolean), codama(name = "foo"))]
199 }];
200 let ctx = AttributeContext::File(&file);
201 let attributes = Attributes::parse(&attrs, ctx).unwrap();
202
203 assert_eq!(attributes.len(), 2);
205 assert!(matches!(&attributes[0], Attribute::Codama(_)));
206 assert!(matches!(&attributes[1], Attribute::Codama(_)));
207
208 if let Attribute::Codama(attr) = &attributes[0] {
210 assert!(matches!(attr.directive.as_ref(), CodamaDirective::Type(_)));
211 }
212 if let Attribute::Codama(attr) = &attributes[1] {
213 assert!(matches!(attr.directive.as_ref(), CodamaDirective::Name(_)));
214 }
215 }
216
217 #[test]
218 fn parse_multi_attr_cfg_attr_mixed_types() {
219 let file = file_ctx();
220 let attrs: Vec<syn::Attribute> = vec![parse_quote! {
222 #[cfg_attr(feature = "x", derive(Debug), codama(type = boolean))]
223 }];
224 let ctx = AttributeContext::File(&file);
225 let attributes = Attributes::parse(&attrs, ctx).unwrap();
226
227 assert_eq!(attributes.len(), 2);
229 assert!(matches!(&attributes[0], Attribute::Derive(_)));
230 assert!(matches!(&attributes[1], Attribute::Codama(_)));
231 }
232
233 #[test]
234 fn parse_multi_attr_cfg_attr_preserves_order() {
235 let file = file_ctx();
236 let attrs: Vec<syn::Attribute> = vec![parse_quote! {
237 #[cfg_attr(feature = "codama", codama(name = "first"), codama(name = "second"), codama(name = "third"))]
238 }];
239 let ctx = AttributeContext::File(&file);
240 let attributes = Attributes::parse(&attrs, ctx).unwrap();
241
242 assert_eq!(attributes.len(), 3);
243
244 let names: Vec<_> = attributes
246 .iter()
247 .filter_map(CodamaAttribute::filter)
248 .filter_map(|a| {
249 if let CodamaDirective::Name(n) = a.directive.as_ref() {
250 Some(n.name.as_ref().to_string())
251 } else {
252 None
253 }
254 })
255 .collect();
256 assert_eq!(names, vec!["first", "second", "third"]);
257 }
258
259 #[test]
260 fn parse_multiple_separate_cfg_attr_and_multi_attr() {
261 let file = file_ctx();
262 let attrs: Vec<syn::Attribute> = vec![
264 parse_quote! { #[derive(Clone)] },
265 parse_quote! { #[cfg_attr(feature = "x", codama(name = "a"), codama(name = "b"))] },
266 parse_quote! { #[codama(type = boolean)] },
267 ];
268 let ctx = AttributeContext::File(&file);
269 let attributes = Attributes::parse(&attrs, ctx).unwrap();
270
271 assert_eq!(attributes.len(), 4);
273 assert!(matches!(&attributes[0], Attribute::Derive(_)));
274 assert!(matches!(&attributes[1], Attribute::Codama(_)));
275 assert!(matches!(&attributes[2], Attribute::Codama(_)));
276 assert!(matches!(&attributes[3], Attribute::Codama(_)));
277 }
278}