syn_ext/
attribute.rs

1use crate::ident::GetIdent;
2#[cfg(feature = "parsing")]
3use crate::meta::{self, MetaExt};
4use crate::meta::{Meta1, MetaList1};
5use syn::{parse_quote, Attribute, Ident};
6#[cfg(feature = "parsing")]
7use syn::{punctuated::Punctuated, token::Paren, Result};
8
9impl GetIdent for Attribute {
10    /// Get ident of the [syn::Attribute::path] field.
11    fn get_ident(&self) -> Option<&Ident> {
12        self.path().get_ident()
13    }
14}
15
16/// Extension for [syn::Attribute]
17#[cfg(feature = "parsing")]
18pub trait AttributeExt {
19    /// Constructs and returns a new [syn::Attribute] from [syn::Meta]
20    fn from_meta<M>(meta: M) -> Self
21    where
22        M: IntoAttribute;
23
24    /// Parses the content of the attribute, consisting of the path and tokens,
25    /// as a [`Meta`][Meta1] if possible.
26    fn parse_meta(&self) -> Result<Meta1>;
27
28    /// Takes a closure and calls it with parsed meta. After call, applys back the manipulated [syn::Meta].
29    ///
30    /// 1. Try [syn::Attribute::parse_meta]; return if `Err`
31    /// 2. Run `f`
32    /// 3. Apply back the manipulated [syn::Meta] by `f`.
33    /// 4. Return the result of `f`.
34    ///
35    /// Note: Even `f` returns `Err`, meta will be made into self.
36    fn try_meta_mut<F, R>(&mut self, f: F) -> Result<R>
37    where
38        F: FnOnce(&mut Meta1) -> Result<R>;
39
40    /// Returns a fake promoted list value of [syn::MetaList].
41    ///
42    /// If [syn::Meta::List], return inner [syn::MetaList].
43    /// If [syn::Meta::Path], return a fake [syn::MetaList] with default paren and empty nested.
44    /// Otherwise return `Err`
45    fn promoted_list(&self) -> Result<MetaList1>;
46
47    /// Takes a closure and calls it with promoted list of parsed meta. After call, applys back the manipulated [syn::MetaList].
48    ///
49    /// 1. Try [syn::Attribute::parse_meta]; return if `Err`
50    /// 2. Promote to [syn::Meta::List] if [syn::Meta::Path]
51    /// 3. Run `f` to inner [syn::MetaList]
52    /// 4. Apply back the manipulated [syn::Meta] by `f`.
53    /// 5. Return the result of `f`.
54    fn try_promoted_list_mut<F, R>(&mut self, paren: Paren, f: F) -> Result<R>
55    where
56        F: FnOnce(&mut MetaList1) -> Result<R>;
57}
58
59#[cfg(feature = "parsing")]
60impl AttributeExt for Attribute {
61    fn from_meta<M>(meta: M) -> Self
62    where
63        M: IntoAttribute,
64    {
65        meta.into_attribute()
66    }
67
68    fn parse_meta(&self) -> Result<Meta1> {
69        use std::convert::TryInto;
70        self.meta.clone().try_into()
71    }
72
73    fn try_meta_mut<F, R>(&mut self, f: F) -> Result<R>
74    where
75        F: FnOnce(&mut Meta1) -> Result<R>,
76    {
77        let mut meta = self.parse_meta()?;
78        let result = f(&mut meta);
79        *self = Self::from_meta(meta);
80        result
81    }
82
83    fn promoted_list(&self) -> Result<MetaList1> {
84        match self.parse_meta()? {
85            Meta1::Path(path) => Ok(MetaList1 {
86                path,
87                paren_token: Default::default(),
88                nested: Punctuated::new(),
89            }),
90            Meta1::List(metalist) => Ok(metalist),
91            other => Err(meta::err_promote_to_list(&other)),
92        }
93    }
94
95    fn try_promoted_list_mut<F, R>(&mut self, paren: Paren, f: F) -> Result<R>
96    where
97        F: FnOnce(&mut MetaList1) -> Result<R>,
98    {
99        self.try_meta_mut(|meta| {
100            let metalist = meta.promote_to_list(paren)?;
101            f(metalist)
102        })
103    }
104}
105
106/// Extension for `std::iter::Iterator<[syn::Attribute]>`
107#[cfg(feature = "parsing")]
108pub trait AttributeIteratorExt {
109    // fn doc_items(&self) -> impl std::iter::Iterator<Item=String>;
110    /// Constructs and returns doc comment string by joining doc from multiple attrs
111    fn doc(self) -> Option<String>;
112
113    // fn filter_name<'a, P>(
114    //     &'a self,
115    //     name: &'static str,
116    // ) -> std::iter::Filter<std::slice::Iter<'a, Attribute>, P>
117    // where
118    //     P: FnMut(&&'a syn::Attribute) -> bool;
119}
120
121#[cfg(feature = "parsing")]
122impl<'a, I> AttributeIteratorExt for I
123where
124    I: std::iter::IntoIterator<Item = &'a Attribute>,
125{
126    fn doc(self) -> Option<String> {
127        let items: Vec<_> = self
128            .into_iter()
129            .filter_map(|attr| attr.parse_meta().ok().and_then(|m| m.doc().ok()))
130            .collect();
131        if items.is_empty() {
132            None
133        } else {
134            Some(items.join("\n"))
135        }
136    }
137
138    // fn filter_name<'a, P>(
139    //     &'a self,
140    //     name: &'static str,
141    // ) -> std::iter::Filter<std::slice::Iter<'a, Attribute>, P>
142    // where
143    //     P: FnMut(&&'a syn::Attribute) -> bool,
144    // {
145    //     let mut p = |attr: &&'a Attribute| {
146    //         attr.path
147    //             .get_ident()
148    //             .map_or(false, |ident| ident.to_string() == name)
149    //     };
150    //     self.iter().filter(p)
151    // }
152}
153
154#[cfg(test)]
155mod test {
156    use super::*;
157    use crate::assert_quote_eq;
158    use quote::quote;
159    use Meta1 as Meta;
160
161    #[cfg(feature = "parsing")]
162    fn test_meta_round_trip(attr: Attribute) -> Result<()> {
163        let meta = attr.parse_meta()?;
164        let created = Attribute::from_meta(meta);
165        assert_eq!(
166            quote! { #attr }.to_string(),
167            quote! { #created }.to_string()
168        );
169        Ok(())
170    }
171
172    #[cfg(feature = "parsing")]
173    #[test]
174    fn run_test_meta_round_trip() {
175        use syn::parse_quote;
176        test_meta_round_trip(parse_quote! { #[cfg(test)] }).unwrap();
177        test_meta_round_trip(parse_quote! { #[feature = "full"] }).unwrap();
178        test_meta_round_trip(parse_quote! { #[cfg(all(a,b,any(c,d)))] }).unwrap();
179        test_meta_round_trip(parse_quote! { #[a(b="1",d)] }).unwrap();
180        test_meta_round_trip(parse_quote! { #[abc::de::ef] }).unwrap();
181    }
182
183    #[cfg(feature = "parsing")]
184    #[test]
185    fn test_try_meta_mut() {
186        let mut attr: Attribute = parse_quote! { #[cfg(test)] };
187        attr.try_meta_mut(|meta| match meta {
188            Meta::List(metalist) => {
189                metalist.path = parse_quote! { newcfg };
190                Ok(())
191            }
192            _ => unreachable!(),
193        })
194        .unwrap();
195        let expected: Attribute = parse_quote! { #[newcfg(test)] };
196        assert_quote_eq!(attr, expected);
197
198        attr.try_meta_mut(|meta| match meta {
199            Meta::List(metalist) => {
200                metalist.nested.pop();
201                metalist.nested.push(parse_quote!(a));
202                metalist.nested.push(parse_quote!(b = "c"));
203                metalist.nested.push(parse_quote!("d"));
204                Ok(())
205            }
206            _ => unreachable!(),
207        })
208        .unwrap();
209        let expected: Attribute = parse_quote! { #[newcfg(a, b="c", "d")] };
210        assert_quote_eq!(attr, expected);
211    }
212
213    #[test]
214    #[cfg(feature = "parsing")]
215    fn test_promoted_list() {
216        let attr: Attribute = parse_quote! { #[derive] };
217        let list = attr.promoted_list().unwrap();
218        assert_quote_eq!(attr.path(), list.path);
219        assert!(list.nested.is_empty());
220    }
221
222    #[cfg(all(feature = "parsing", feature = "full"))]
223    #[test]
224    fn test_doc() {
225        let func: syn::ItemFn = parse_quote! {
226            #[derive]
227            /// doc line 1
228            #[test]
229            #[doc = "doc line 2"]
230            #[cfg]
231            fn f() {}
232        };
233        let doc = func.attrs.doc().unwrap();
234        assert_eq!(doc, "doc line 1\ndoc line 2");
235    }
236}
237
238pub trait IntoAttribute {
239    fn into_attribute(self) -> Attribute;
240}
241
242impl IntoAttribute for Meta1 {
243    fn into_attribute(self) -> Attribute {
244        parse_quote!( #[ #self ] )
245    }
246}
247
248impl IntoAttribute for MetaList1 {
249    fn into_attribute(self) -> Attribute {
250        Meta1::List(self).into_attribute()
251    }
252}