codama_attributes/
attributes.rs1use 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 .flat_map(|ast| {
19 let inners = ast.unfeatured_all();
20 if inners.len() <= 1 {
21 let unfeatured = ast.unfeatured();
23 let effective = unfeatured.unwrap_or_else(|| (*ast).clone());
24 vec![(ast, effective)]
25 } else {
26 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 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 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 assert_eq!(attributes.len(), 2);
214 assert!(matches!(&attributes[0], Attribute::Codama(_)));
215 assert!(matches!(&attributes[1], Attribute::Codama(_)));
216
217 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 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 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 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 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 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}