gtk4_macros/
attribute_parser.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use proc_macro2::Span;
4use quote::ToTokens;
5use syn::{
6    parse::{Parse, ParseStream},
7    punctuated::Punctuated,
8    Attribute, DeriveInput, Error, Field, Fields, Ident, LitBool, LitStr, Meta, Result, Token,
9    Type,
10};
11
12/// Custom meta keywords.
13mod kw {
14    // `template` attribute.
15    syn::custom_keyword!(file);
16    syn::custom_keyword!(resource);
17    syn::custom_keyword!(string);
18    syn::custom_keyword!(allow_template_child_without_attribute);
19
20    // `template_child` attribute.
21    syn::custom_keyword!(id);
22    syn::custom_keyword!(internal);
23}
24
25/// The parsed `template` attribute.
26pub struct Template {
27    pub source: TemplateSource,
28    pub allow_template_child_without_attribute: bool,
29}
30
31impl Parse for Template {
32    fn parse(input: ParseStream) -> Result<Self> {
33        let mut source = None;
34        let mut allow_template_child_without_attribute = false;
35
36        while !input.is_empty() {
37            let lookahead = input.lookahead1();
38            if lookahead.peek(kw::file) {
39                let keyword: kw::file = input.parse()?;
40                let _: Token![=] = input.parse()?;
41                let value: LitStr = input.parse()?;
42
43                if source.is_some() {
44                    return Err(Error::new_spanned(
45                        keyword,
46                        "Specify only one of 'file', 'resource', or 'string'",
47                    ));
48                }
49
50                source = Some(TemplateSource::File(value.value()));
51            } else if lookahead.peek(kw::resource) {
52                let keyword: kw::resource = input.parse()?;
53                let _: Token![=] = input.parse()?;
54                let value: LitStr = input.parse()?;
55
56                if source.is_some() {
57                    return Err(Error::new_spanned(
58                        keyword,
59                        "Specify only one of 'file', 'resource', or 'string'",
60                    ));
61                }
62
63                source = Some(TemplateSource::Resource(value.value()));
64            } else if lookahead.peek(kw::string) {
65                let keyword: kw::string = input.parse()?;
66                let _: Token![=] = input.parse()?;
67                let value: LitStr = input.parse()?;
68
69                if source.is_some() {
70                    return Err(Error::new_spanned(
71                        keyword,
72                        "Specify only one of 'file', 'resource', or 'string'",
73                    ));
74                }
75
76                source = Some(
77                    TemplateSource::from_string_source(value.value())
78                        .ok_or_else(|| Error::new_spanned(value, "Unknown language"))?,
79                );
80            } else if lookahead.peek(kw::allow_template_child_without_attribute) {
81                let keyword: kw::allow_template_child_without_attribute = input.parse()?;
82
83                if allow_template_child_without_attribute {
84                    return Err(Error::new_spanned(
85                        keyword,
86                        "Duplicate 'allow_template_child_without_attribute'",
87                    ));
88                }
89
90                allow_template_child_without_attribute = true;
91            } else {
92                return Err(lookahead.error());
93            }
94
95            if !input.is_empty() {
96                let _: Token![,] = input.parse()?;
97            }
98        }
99
100        let Some(source) = source else {
101            return Err(Error::new(
102                Span::call_site(),
103                "Invalid meta, specify one of 'file', 'resource', or 'string'",
104            ));
105        };
106
107        Ok(Template {
108            source,
109            allow_template_child_without_attribute,
110        })
111    }
112}
113
114/// The source of a template.
115pub enum TemplateSource {
116    File(String),
117    Resource(String),
118    Xml(String),
119    #[cfg(feature = "blueprint")]
120    Blueprint(String),
121}
122
123impl TemplateSource {
124    fn from_string_source(value: String) -> Option<Self> {
125        for c in value.chars() {
126            #[cfg(feature = "blueprint")]
127            if c.is_ascii_alphabetic() {
128                // blueprint code starts with some alphabetic letters
129                return Some(Self::Blueprint(value));
130            } else if c == '<' {
131                // xml tags starts with '<' symbol
132                return Some(Self::Xml(value));
133            }
134            #[cfg(not(feature = "blueprint"))]
135            if c == '<' {
136                // xml tags starts with '<' symbol
137                return Some(Self::Xml(value));
138            }
139        }
140
141        None
142    }
143}
144
145pub fn parse_template_source(input: &DeriveInput) -> Result<Template> {
146    let Some(attr) = input
147        .attrs
148        .iter()
149        .find(|attr| attr.path().is_ident("template"))
150    else {
151        return Err(Error::new(
152            Span::call_site(),
153            "Missing 'template' attribute",
154        ));
155    };
156
157    attr.parse_args::<Template>()
158}
159
160/// An argument in a field attribute.
161pub enum FieldAttributeArg {
162    #[allow(dead_code)]
163    // The span is needed for xml_validation feature
164    Id(String, Span),
165    Internal(bool),
166}
167
168impl FieldAttributeArg {
169    fn from_template_child_meta(meta: &TemplateChildAttributeMeta) -> Self {
170        match meta {
171            TemplateChildAttributeMeta::Id { value, .. } => Self::Id(value.value(), value.span()),
172            TemplateChildAttributeMeta::Internal { value, .. } => Self::Internal(value.value()),
173        }
174    }
175}
176
177/// The type of a field attribute.
178#[derive(Debug)]
179pub enum FieldAttributeType {
180    TemplateChild,
181}
182
183/// A field attribute with args.
184pub struct FieldAttribute {
185    pub ty: FieldAttributeType,
186    pub args: Vec<FieldAttributeArg>,
187}
188
189/// A field with an attribute.
190pub struct AttributedField {
191    pub ident: Ident,
192    pub ty: Type,
193    pub attr: FieldAttribute,
194}
195
196impl AttributedField {
197    pub fn id(&self) -> String {
198        let mut name = None;
199        for arg in &self.attr.args {
200            if let FieldAttributeArg::Id(value, _) = arg {
201                name = Some(value)
202            }
203        }
204        name.cloned().unwrap_or_else(|| self.ident.to_string())
205    }
206
207    #[cfg(feature = "xml_validation")]
208    pub fn id_span(&self) -> Span {
209        for arg in &self.attr.args {
210            if let FieldAttributeArg::Id(_, span) = arg {
211                return *span;
212            }
213        }
214        self.ident.span()
215    }
216}
217
218/// A meta item of a `template_child` attribute.
219enum TemplateChildAttributeMeta {
220    Id {
221        keyword: kw::id,
222        value: LitStr,
223    },
224    Internal {
225        keyword: kw::internal,
226        value: LitBool,
227    },
228}
229
230impl Parse for TemplateChildAttributeMeta {
231    fn parse(input: ParseStream) -> Result<Self> {
232        let lookahead = input.lookahead1();
233        if lookahead.peek(kw::id) {
234            let keyword = input.parse()?;
235            let _: Token![=] = input.parse()?;
236            let value = input.parse()?;
237            Ok(Self::Id { keyword, value })
238        } else if lookahead.peek(kw::internal) {
239            let keyword = input.parse()?;
240            let _: Token![=] = input.parse()?;
241            let value = input.parse()?;
242            Ok(Self::Internal { keyword, value })
243        } else {
244            Err(lookahead.error())
245        }
246    }
247}
248
249impl ToTokens for TemplateChildAttributeMeta {
250    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
251        match self {
252            Self::Id { keyword, .. } => keyword.to_tokens(tokens),
253            Self::Internal { keyword, .. } => keyword.to_tokens(tokens),
254        }
255    }
256}
257
258fn parse_field_attr_args(ty: FieldAttributeType, attr: &Attribute) -> Result<FieldAttribute> {
259    let mut args = Vec::new();
260
261    if matches!(ty, FieldAttributeType::TemplateChild) && !matches!(attr.meta, Meta::Path(_)) {
262        let meta_list = attr.parse_args_with(
263            Punctuated::<TemplateChildAttributeMeta, Token![,]>::parse_terminated,
264        )?;
265
266        for meta in meta_list {
267            let new_arg = FieldAttributeArg::from_template_child_meta(&meta);
268
269            if args.iter().any(|arg| {
270                // Comparison of enum variants, not data
271                std::mem::discriminant(arg) == std::mem::discriminant(&new_arg)
272            }) {
273                return Err(Error::new_spanned(
274                    meta,
275                    "two instances of the same attribute \
276                    argument, each argument must be specified only once",
277                ));
278            }
279
280            args.push(new_arg);
281        }
282    }
283
284    Ok(FieldAttribute { ty, args })
285}
286
287fn parse_field(field: &Field) -> Result<Option<AttributedField>> {
288    let Some(ident) = &field.ident else {
289        return Err(Error::new_spanned(field, "expected identifier"));
290    };
291
292    let mut attr_field = None;
293
294    for attr in &field.attrs {
295        let ty = if attr.path().is_ident("template_child") {
296            FieldAttributeType::TemplateChild
297        } else {
298            continue;
299        };
300
301        let field_attr = parse_field_attr_args(ty, attr)?;
302
303        if attr_field.is_some() {
304            return Err(Error::new_spanned(
305                attr,
306                "multiple attributes on the same field are not supported",
307            ));
308        }
309
310        attr_field = Some(AttributedField {
311            ident: ident.clone(),
312            ty: field.ty.clone(),
313            attr: field_attr,
314        })
315    }
316
317    Ok(attr_field)
318}
319
320fn path_is_template_child(path: &syn::Path) -> bool {
321    if path.leading_colon.is_none()
322        && path.segments.len() == 1
323        && matches!(
324            &path.segments[0].arguments,
325            syn::PathArguments::AngleBracketed(_)
326        )
327        && path.segments[0].ident == "TemplateChild"
328    {
329        return true;
330    }
331    if path.segments.len() == 2
332        && (path.segments[0].ident == "gtk" || path.segments[0].ident == "gtk4")
333        && matches!(
334            &path.segments[1].arguments,
335            syn::PathArguments::AngleBracketed(_)
336        )
337        && path.segments[1].ident == "TemplateChild"
338    {
339        return true;
340    }
341    false
342}
343
344pub fn parse_fields(
345    fields: &Fields,
346    allow_missing_attribute: bool,
347) -> Result<Vec<AttributedField>> {
348    let mut attributed_fields = Vec::new();
349
350    for field in fields {
351        let mut has_attr = false;
352        if !field.attrs.is_empty() {
353            if let Some(attributed_field) = parse_field(field)? {
354                attributed_fields.push(attributed_field);
355                has_attr = true;
356            }
357        }
358        if !has_attr && !allow_missing_attribute {
359            if let syn::Type::Path(syn::TypePath { path, .. }) = &field.ty {
360                if path_is_template_child(path) {
361                    return Err(Error::new_spanned(
362                        field,
363                        format!("field `{}` with type `TemplateChild` possibly missing #[template_child] attribute. Use a meta attribute on the struct to suppress this error: '#[template(string|file|resource = \"...\", allow_template_child_without_attribute)]'",
364                        field.ident.as_ref().unwrap())
365                    ));
366                }
367            }
368        }
369    }
370
371    Ok(attributed_fields)
372}