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("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 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 assert_eq!(attributes.len(), 2);
207 assert!(matches!(&attributes[0], Attribute::Codama(_)));
208 assert!(matches!(&attributes[1], Attribute::Codama(_)));
209
210 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 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 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 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 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 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}