glib_macros/
utils.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use proc_macro2::{Ident, Span, TokenStream};
4use quote::{quote, quote_spanned, ToTokens};
5use syn::{
6    meta::ParseNestedMeta, parse::Parse, punctuated::Punctuated, spanned::Spanned, token::Comma,
7    Token, Variant,
8};
9
10pub trait ParseNestedMetaItem {
11    fn get_name(&self) -> &'static str;
12    fn get_found(&self) -> bool;
13    fn get_required(&self) -> bool;
14    fn parse_nested(&mut self, meta: &ParseNestedMeta) -> Option<syn::Result<()>>;
15}
16
17#[derive(Default)]
18pub struct NestedMetaItem<T> {
19    pub name: &'static str,
20    pub value_required: bool,
21    pub found: bool,
22    pub required: bool,
23    pub value: Option<T>,
24}
25
26impl<T: Parse + ToTokens> std::fmt::Debug for NestedMetaItem<T> {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("NestedMetaItem")
29            .field("name", &self.name)
30            .field("required", &self.required)
31            .field("value_required", &self.value_required)
32            .field("found", &self.found)
33            .field("value", &self.value.as_ref().map(|v| quote!(#v)))
34            .finish()
35    }
36}
37
38impl<T: Parse> NestedMetaItem<T> {
39    pub const fn new(name: &'static str) -> Self {
40        Self {
41            required: false,
42            name,
43            found: false,
44            value_required: false,
45            value: None,
46        }
47    }
48    pub fn required(mut self) -> Self {
49        self.required = true;
50        self
51    }
52    // Note: this flags the `value` as required, that is,
53    // the parameter after the equal: `name = value`.
54    pub const fn value_required(mut self) -> Self {
55        self.value_required = true;
56        self
57    }
58    pub const fn value_optional(mut self) -> Self {
59        self.value_required = false;
60        self
61    }
62    fn parse_nested_forced(&mut self, meta: &ParseNestedMeta) -> syn::Result<()> {
63        if self.value_required || meta.input.peek(Token![=]) {
64            let _eq: Token![=] = meta.input.parse()?;
65            self.value = Some(meta.input.parse()?);
66        }
67        Ok(())
68    }
69}
70impl<T: Parse> ParseNestedMetaItem for NestedMetaItem<T> {
71    fn get_name(&self) -> &'static str {
72        self.name
73    }
74    fn parse_nested(&mut self, meta: &ParseNestedMeta) -> Option<syn::Result<()>> {
75        if meta.path.is_ident(self.name) {
76            self.found = true;
77            Some(self.parse_nested_forced(meta))
78        } else {
79            None
80        }
81    }
82    fn get_found(&self) -> bool {
83        self.found
84    }
85    fn get_required(&self) -> bool {
86        self.required
87    }
88}
89
90pub fn check_meta_items(span: Span, items: &mut [&mut dyn ParseNestedMetaItem]) -> syn::Result<()> {
91    let mut err: Option<syn::Error> = None;
92    for item in &mut *items {
93        if item.get_required() && !item.get_found() {
94            let nerr = syn::Error::new(
95                span,
96                format!("attribute `{}` must be specified", item.get_name()),
97            );
98            if let Some(ref mut err) = err {
99                err.combine(nerr);
100            } else {
101                err = Some(nerr);
102            }
103        }
104    }
105    match err {
106        Some(err) => Err(err),
107        None => Ok(()),
108    }
109}
110fn parse_nested_meta_items_from_fn(
111    parse_nested_meta: impl FnOnce(
112        &mut dyn FnMut(ParseNestedMeta) -> syn::Result<()>,
113    ) -> syn::Result<()>,
114    items: &mut [&mut dyn ParseNestedMetaItem],
115) -> syn::Result<()> {
116    parse_nested_meta(&mut |meta| {
117        for item in &mut *items {
118            if let Some(res) = item.parse_nested(&meta) {
119                return res;
120            }
121        }
122        Err(meta.error(format!(
123            "unknown attribute `{}`. Possible attributes are {}",
124            meta.path.get_ident().unwrap(),
125            items
126                .iter()
127                .map(|i| format!("`{}`", i.get_name()))
128                .collect::<Vec<_>>()
129                .join(", ")
130        )))
131    })?;
132    Ok(())
133}
134
135pub fn parse_nested_meta_items_from_stream(
136    input: TokenStream,
137    items: &mut [&mut dyn ParseNestedMetaItem],
138) -> syn::Result<()> {
139    parse_nested_meta_items_from_fn(
140        |f| {
141            let p = syn::meta::parser(f);
142            syn::parse::Parser::parse(p, input.into())
143        },
144        items,
145    )?;
146    check_meta_items(Span::call_site(), items)
147}
148
149pub fn parse_nested_meta_items<'a>(
150    attrs: impl IntoIterator<Item = &'a syn::Attribute>,
151    attr_name: &str,
152    items: &mut [&mut dyn ParseNestedMetaItem],
153) -> syn::Result<Option<&'a syn::Attribute>> {
154    let attr = attrs
155        .into_iter()
156        .find(|attr| attr.path().is_ident(attr_name));
157    if let Some(attr) = attr {
158        parse_nested_meta_items_from_fn(|x| attr.parse_nested_meta(x), items)?;
159        check_meta_items(attr.span(), items)?;
160        Ok(Some(attr))
161    } else {
162        Ok(None)
163    }
164}
165
166pub fn parse_optional_nested_meta_items<'a>(
167    attrs: impl IntoIterator<Item = &'a syn::Attribute>,
168    attr_name: &str,
169    items: &mut [&mut dyn ParseNestedMetaItem],
170) -> syn::Result<Option<&'a syn::Attribute>> {
171    let attr = attrs
172        .into_iter()
173        .find(|attr| attr.path().is_ident(attr_name));
174    if let Some(attr) = attr {
175        if let syn::Meta::Path(_) = attr.meta {
176            Ok(Some(attr))
177        } else {
178            parse_nested_meta_items_from_fn(|x| attr.parse_nested_meta(x), items)?;
179            check_meta_items(attr.span(), items)?;
180            Ok(Some(attr))
181        }
182    } else {
183        Ok(None)
184    }
185}
186
187#[cfg(feature = "proc_macro_crate")]
188pub fn crate_ident_new() -> TokenStream {
189    use proc_macro_crate::{crate_name, FoundCrate};
190
191    match crate_name("glib") {
192        Ok(FoundCrate::Name(name)) => Some(name),
193        Ok(FoundCrate::Itself) => Some("glib".to_string()),
194        Err(_) => None,
195    }
196    .map(|s| {
197        let glib = Ident::new(&s, Span::call_site());
198        quote!(#glib)
199    })
200    .unwrap_or_else(|| {
201        // We couldn't find the glib crate (renamed or not) so let's just hope it's in scope!
202        //
203        // We will be able to have this information once this code is stable:
204        //
205        // ```
206        // let span = Span::call_site();
207        // let source = span.source_file();
208        // let file_path = source.path();
209        // ```
210        //
211        // Then we can use proc_macro to parse the file and check if glib is imported somehow.
212        let glib = Ident::new("glib", Span::call_site());
213        quote!(#glib)
214    })
215}
216
217#[cfg(not(feature = "proc_macro_crate"))]
218pub fn crate_ident_new() -> TokenStream {
219    let glib = Ident::new("glib", Span::call_site());
220    quote!(#glib)
221}
222
223// Generate i32 to enum mapping, used to implement
224// glib::translate::TryFromGlib<i32>, such as:
225//
226//   if value == Animal::Goat as i32 {
227//       return Some(Animal::Goat);
228//   }
229pub fn gen_enum_from_glib(
230    enum_name: &Ident,
231    enum_variants: &Punctuated<Variant, Comma>,
232) -> TokenStream {
233    // FIXME: can we express this with a match()?
234    let recurse = enum_variants.iter().map(|v| {
235        let name = &v.ident;
236        quote_spanned! { v.span() =>
237            if value == #enum_name::#name as i32 {
238                return ::core::option::Option::Some(#enum_name::#name);
239            }
240        }
241    });
242    quote! {
243        #(#recurse)*
244        ::core::option::Option::None
245    }
246}
247
248// These tests are useful to pinpoint the exact location of a macro panic
249// by running `cargo test --lib`
250#[cfg(test)]
251mod tests {
252    use syn::{parse_quote, DeriveInput};
253
254    use super::*;
255
256    fn boxed_stub() -> DeriveInput {
257        parse_quote!(
258            #[boxed_type(name = "Author")]
259            struct Author {
260                name: String,
261            }
262        )
263    }
264
265    #[test]
266    fn check_attr_found() {
267        let input = boxed_stub();
268        let found = parse_nested_meta_items(&input.attrs, "boxed_type", &mut []);
269        matches!(found, Ok(Some(_)));
270    }
271    #[test]
272    fn required_name_present() {
273        let input = boxed_stub();
274        let mut gtype_name = NestedMetaItem::<syn::LitStr>::new("name")
275            .required()
276            .value_required();
277        let _ = parse_nested_meta_items(&input.attrs, "boxed_type", &mut [&mut gtype_name]);
278        assert!(gtype_name.get_found());
279        assert_eq!(
280            gtype_name.value.map(|x| x.value()),
281            Some("Author".to_string())
282        );
283    }
284    #[test]
285    fn required_name_none() {
286        let input: DeriveInput = parse_quote!(
287            #[boxed_type(name)]
288            struct Author {
289                name: String,
290            }
291        );
292        let mut gtype_name = NestedMetaItem::<syn::LitStr>::new("name")
293            .required()
294            .value_required();
295        let found = parse_nested_meta_items(&input.attrs, "boxed_type", &mut [&mut gtype_name]);
296        // The argument value was specified as required, so an error is returned
297        assert!(found.is_err());
298        assert!(gtype_name.value.is_none());
299
300        // The argument key must be found though
301        assert!(gtype_name.get_found());
302    }
303}