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