cxx_qt_gen/parser/
mod.rs

1// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6pub mod constructor;
7pub mod cxxqtdata;
8pub mod externcxxqt;
9pub mod externqobject;
10pub mod inherit;
11pub mod method;
12pub mod parameter;
13pub mod property;
14pub mod qenum;
15pub mod qnamespace;
16pub mod qobject;
17pub mod signals;
18pub mod trait_impl;
19
20use crate::{
21    naming::TypeNames,
22    syntax::{expr::expr_to_string, path::path_compare_str},
23};
24use convert_case::Case;
25use cxxqtdata::ParsedCxxQtData;
26use std::collections::BTreeMap;
27use syn::{
28    punctuated::Punctuated,
29    spanned::Spanned,
30    token::{Brace, Semi},
31    Attribute, Error, Expr, Ident, Item, ItemMod, Meta, Result, Token, Visibility,
32};
33
34#[derive(Copy, Clone)]
35pub struct CaseConversion {
36    pub cxx: Option<Case>,
37    pub rust: Option<Case>,
38}
39
40/// Used to match the auto_case attributes and turn it into a Case to convert to
41fn meta_to_case(attr: &Attribute, default: Case) -> Result<Case> {
42    match &attr.meta {
43        Meta::Path(_) => Ok(default),
44        Meta::NameValue(case) => match &case.value {
45            Expr::Path(expr_path) => match expr_path.path.require_ident()?.to_string().as_str() {
46                "Camel" => Ok(Case::Camel),
47                "Snake" => Ok(Case::Snake),
48                _ => Err(Error::new(
49                    attr.span(),
50                    "Invalid case! You can use either `Camel` or `Snake`",
51                )),
52            },
53            _ => Err(Error::new(
54                attr.span(),
55                "Case should be specified as an identifier! Like `#[auto_cxx_name = Camel]`",
56            )),
57        },
58        _ => Err(Error::new(
59            attr.span(),
60            "Invalid attribute format! Use like `auto_cxx_name` or `auto_cxx_name = Camel`",
61        )),
62    }
63}
64
65impl CaseConversion {
66    pub fn none() -> Self {
67        Self {
68            cxx: None,
69            rust: None,
70        }
71    }
72
73    /// Create a CaseConversion object from a Map of attributes, collected using `require_attributes`
74    /// Parses both `auto_cxx_name` and `auto_cxx_name = Camel`
75    pub fn from_attrs(attrs: &BTreeMap<&str, &Attribute>) -> Result<Self> {
76        let rust = attrs
77            .get("auto_rust_name")
78            .map(|attr| meta_to_case(attr, Case::Snake))
79            .transpose()?;
80        let cxx = attrs
81            .get("auto_cxx_name")
82            .map(|attr| meta_to_case(attr, Case::Camel))
83            .transpose()?;
84
85        Ok(Self { rust, cxx })
86    }
87}
88
89/// Iterate the attributes of the method to extract cfg attributes
90pub fn extract_cfgs(attrs: &[Attribute]) -> Vec<Attribute> {
91    attrs
92        .iter()
93        .filter(|attr| path_compare_str(attr.meta.path(), &["cfg"]))
94        .cloned()
95        .collect()
96}
97
98/// Iterate the attributes of the method to extract Doc attributes (doc comments are parsed as this)
99pub fn extract_docs(attrs: &[Attribute]) -> Vec<Attribute> {
100    attrs
101        .iter()
102        .filter(|attr| path_compare_str(attr.meta.path(), &["doc"]))
103        .cloned()
104        .collect()
105}
106
107/// Splits a path by :: separators e.g. "cxx_qt::bridge" becomes ["cxx_qt", "bridge"]
108fn split_path(path_str: &str) -> Vec<&str> {
109    let path = if path_str.contains("::") {
110        path_str.split("::").collect::<Vec<_>>()
111    } else {
112        vec![path_str]
113    };
114    path
115}
116
117/// Collects a Map of all attributes found from the allowed list
118/// Will error if an attribute which is not in the allowed list is found
119pub fn require_attributes<'a>(
120    attrs: &'a [Attribute],
121    allowed: &'a [&str],
122) -> Result<BTreeMap<&'a str, &'a Attribute>> {
123    let mut output = BTreeMap::default();
124    for attr in attrs {
125        let index = allowed
126            .iter()
127            .position(|string| path_compare_str(attr.meta.path(), &split_path(string)));
128        if let Some(index) = index {
129            output.insert(allowed[index], attr); // Doesn't error on duplicates
130        } else {
131            return Err(Error::new(
132                attr.span(),
133                format!(
134                    "Unsupported attribute! The only attributes allowed on this item are\n{}",
135                    allowed.join(", ")
136                ),
137            ));
138        }
139    }
140    Ok(output)
141}
142
143/// Struct representing the necessary components of a cxx mod to be passed through to generation
144pub struct PassthroughMod {
145    pub(crate) items: Option<Vec<Item>>,
146    pub(crate) docs: Vec<Attribute>,
147    pub(crate) module_ident: Ident,
148    pub(crate) vis: Visibility,
149}
150
151impl PassthroughMod {
152    /// Parse an item mod into it's components
153    pub fn parse(module: ItemMod) -> Self {
154        let items = module.content.map(|(_, items)| items);
155
156        Self {
157            items,
158            docs: extract_docs(&module.attrs),
159            module_ident: module.ident,
160            vis: module.vis,
161        }
162    }
163}
164
165/// A struct representing a module block with CXX-Qt relevant [syn::Item]'s
166/// parsed into ParsedCxxQtData, to be used later to generate Rust & C++ code.
167///
168/// [syn::Item]'s that are not handled specially by CXX-Qt are passed through for CXX to process.
169pub struct Parser {
170    /// The module which unknown (eg CXX) blocks are stored into
171    pub(crate) passthrough_module: PassthroughMod,
172    /// Any CXX-Qt data that needs generation later
173    pub(crate) cxx_qt_data: ParsedCxxQtData,
174    /// all type names that were found in this module, including CXX types
175    pub(crate) type_names: TypeNames,
176}
177
178impl Parser {
179    fn parse_mod_attributes(module: &mut ItemMod) -> Result<Option<String>> {
180        let attrs = require_attributes(&module.attrs, &["doc", "cxx_qt::bridge"])?;
181        let mut namespace = None;
182
183        // Check for the cxx_qt::bridge attribute
184        if let Some(attr) = attrs.get("cxx_qt::bridge") {
185            // If we are not #[cxx_qt::bridge] but #[cxx_qt::bridge(A = B)] then process
186            if !matches!(attr.meta, Meta::Path(_)) {
187                let nested =
188                    attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;
189                for meta in nested {
190                    match meta {
191                        Meta::NameValue(ref name_value) => {
192                            // Parse any namespace in the cxx_qt::bridge macro
193                            if name_value.path.is_ident("namespace") {
194                                namespace = Some(expr_to_string(&name_value.value)?);
195                                // Parse any custom file stem
196                            } else if name_value.path.is_ident("cxx_file_stem") {
197                                return Err(Error::new(
198                                    meta.span(),
199                                    "cxx_file_stem is unsupported, instead the input file name will be used",
200                                ));
201                            }
202                        }
203                        _others => {}
204                    }
205                }
206            }
207        } else {
208            return Err(Error::new(
209                module.span(),
210                "Tried to parse a module which doesn't have a cxx_qt::bridge attribute!",
211            ));
212        }
213
214        Ok(namespace)
215    }
216
217    fn parse_module_contents(
218        mut module: ItemMod,
219        namespace: Option<String>,
220    ) -> Result<(ParsedCxxQtData, ItemMod)> {
221        let mut others = vec![];
222
223        let mut cxx_qt_data = ParsedCxxQtData::new(module.ident.clone(), namespace);
224
225        // Check that there are items in the module
226        if let Some((_, items)) = module.content {
227            // Loop through items and load into qobject or others and populate mappings
228            for item in items.into_iter() {
229                // Try to find any CXX-Qt items, if found add them to the relevant
230                // qobject or extern C++Qt block. Otherwise return them to be added to other
231                if let Some(other) = cxx_qt_data.parse_cxx_qt_item(item)? {
232                    // Unknown item so add to the other list
233                    others.push(other);
234                }
235            }
236        }
237
238        // Create a new module using only items that are not CXX-Qt items
239        if !others.is_empty() {
240            module.content = Some((Brace::default(), others));
241            module.semi = None;
242        } else {
243            module.content = None;
244            module.semi = Some(Semi::default());
245        }
246        Ok((cxx_qt_data, module))
247    }
248
249    /// The "Naming phase", it generates a list of all nameable types in our bridge.
250    fn naming_phase(
251        cxx_qt_data: &mut ParsedCxxQtData,
252        cxx_items: &[Item],
253        module_ident: &Ident,
254    ) -> Result<TypeNames> {
255        TypeNames::from_parsed_data(
256            cxx_qt_data,
257            cxx_items,
258            cxx_qt_data.namespace.as_deref(),
259            module_ident,
260        )
261    }
262
263    /// Constructs a Parser object from a given [syn::ItemMod] block
264    pub fn from(mut module: ItemMod) -> Result<Self> {
265        let namespace = Self::parse_mod_attributes(&mut module)?;
266        let (mut cxx_qt_data, module) = Self::parse_module_contents(module, namespace)?;
267        let type_names = Self::naming_phase(
268            &mut cxx_qt_data,
269            module
270                .content
271                .as_ref()
272                .map(|brace_and_items| &brace_and_items.1)
273                .unwrap_or(&vec![]),
274            &module.ident,
275        )?;
276
277        // Return the successful Parser object
278        Ok(Self {
279            passthrough_module: PassthroughMod::parse(module),
280            type_names,
281            cxx_qt_data,
282        })
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    use crate::tests::assert_parse_errors;
291    use pretty_assertions::assert_eq;
292    use quote::format_ident;
293    use syn::{parse_quote, ItemMod, Type};
294
295    /// Helper which returns a f64 as a [syn::Type]
296    pub fn f64_type() -> Type {
297        parse_quote! { f64 }
298    }
299    #[test]
300    fn test_parser_from_empty_module() {
301        let module: ItemMod = parse_quote! {
302            #[cxx_qt::bridge]
303            mod ffi {}
304        };
305        let parser = Parser::from(module).unwrap();
306
307        assert!(parser.passthrough_module.items.is_none());
308        assert!(parser.passthrough_module.docs.is_empty());
309        assert_eq!(parser.passthrough_module.module_ident, "ffi");
310        assert_eq!(parser.passthrough_module.vis, Visibility::Inherited);
311        assert_eq!(parser.cxx_qt_data.namespace, None);
312        assert_eq!(parser.cxx_qt_data.qobjects.len(), 0);
313    }
314
315    #[test]
316    fn test_incorrect_bridge_args() {
317        let module: ItemMod = parse_quote! {
318            #[cxx_qt::bridge(a, b, c)]
319            mod ffi {
320                extern "Rust" {
321                    fn test();
322                }
323            }
324        };
325        assert!(Parser::from(module).is_ok()); // Meta::List args in cxx_qt bridge are ignored
326
327        let module: ItemMod = parse_quote! {
328            #[cxx_qt::bridge(a = b)]
329            mod ffi {
330                extern "Rust" {
331                    fn test();
332                }
333            }
334        };
335        assert!(Parser::from(module).is_ok()); // Meta::NameValue args which aren't `namespace` or `cxx_file_stem` are ignored
336    }
337
338    #[test]
339    fn test_parser_from_cxx_items() {
340        let module: ItemMod = parse_quote! {
341            #[cxx_qt::bridge]
342            mod ffi {
343                extern "Rust" {
344                    fn test();
345                }
346            }
347        };
348        let parser = Parser::from(module).unwrap();
349        assert_eq!(parser.passthrough_module.items.unwrap().len(), 1);
350        assert!(parser.passthrough_module.docs.is_empty());
351        assert_eq!(parser.passthrough_module.module_ident, "ffi");
352        assert_eq!(parser.passthrough_module.vis, Visibility::Inherited);
353        assert_eq!(parser.cxx_qt_data.namespace, None);
354        assert_eq!(parser.cxx_qt_data.qobjects.len(), 0);
355    }
356
357    #[test]
358    fn test_parser_from_cxx_qt_items() {
359        let module: ItemMod = parse_quote! {
360            #[cxx_qt::bridge(namespace = "cxx_qt")]
361            mod ffi {
362                extern "RustQt" {
363                    #[qobject]
364                    type MyObject = super::MyObjectRust;
365                }
366
367                unsafe extern "RustQt" {
368                    #[qsignal]
369                    fn ready(self: Pin<&mut MyObject>);
370                }
371            }
372        };
373        let parser = Parser::from(module.clone()).unwrap();
374
375        assert!(parser.passthrough_module.items.is_none());
376        assert!(parser.passthrough_module.docs.is_empty());
377        assert_eq!(parser.passthrough_module.module_ident, "ffi");
378        assert_eq!(parser.passthrough_module.vis, Visibility::Inherited);
379        assert_eq!(parser.cxx_qt_data.namespace, Some("cxx_qt".to_owned()));
380        assert_eq!(parser.cxx_qt_data.qobjects.len(), 1);
381        assert_eq!(parser.type_names.num_types(), 18);
382        assert_eq!(
383            parser
384                .type_names
385                .rust_qualified(&format_ident!("MyObject"))
386                .unwrap(),
387            parse_quote! { ffi::MyObject }
388        );
389        assert_eq!(
390            parser
391                .type_names
392                .rust_qualified(&format_ident!("MyObjectRust"))
393                .unwrap(),
394            parse_quote! { ffi::MyObjectRust }
395        );
396    }
397
398    #[test]
399    fn test_parser_from_cxx_and_cxx_qt_items() {
400        let module: ItemMod = parse_quote! {
401            #[cxx_qt::bridge]
402            /// A cxx_qt::bridge module
403            mod ffi {
404                extern "RustQt" {
405                    #[qobject]
406                    type MyObject = super::MyObjectRust;
407                }
408
409                unsafe extern "RustQt" {
410                    #[qsignal]
411                    fn ready(self: Pin<&mut MyObject>);
412                }
413
414                extern "Rust" {
415                    fn test();
416                }
417            }
418        };
419        let parser = Parser::from(module.clone()).unwrap();
420
421        assert_eq!(parser.passthrough_module.items.unwrap().len(), 1);
422        assert_eq!(parser.passthrough_module.docs.len(), 1);
423        assert_eq!(parser.passthrough_module.module_ident, "ffi");
424        assert_eq!(parser.passthrough_module.vis, Visibility::Inherited);
425        assert_eq!(parser.cxx_qt_data.namespace, None);
426        assert_eq!(parser.cxx_qt_data.qobjects.len(), 1);
427    }
428
429    #[test]
430    fn test_parser_invalid() {
431        assert_parse_errors! {
432            Parser::from =>
433
434            {
435                // Non-string namespace
436                #[cxx_qt::bridge]
437                mod ffi {
438                    extern "Rust" {
439                        #[namespace = 1]
440                        type MyObject = super::MyObjectRust;
441                    }
442                }
443            }
444            {
445                // No cxx_qt bridge on module
446                mod ffi {
447                    extern "Rust" {
448                        fn test();
449                    }
450                }
451            }
452            {
453                // Cxx_file_stem is deprecated
454                #[cxx_qt::bridge(cxx_file_stem = "stem")]
455                mod ffi {
456                    extern "Rust" {
457                        fn test();
458                    }
459                }
460            }
461        }
462    }
463
464    #[test]
465    fn test_cxx_qobject_namespace() {
466        let module: ItemMod = parse_quote! {
467            #[cxx_qt::bridge(namespace = "bridge_namespace")]
468            mod ffi {
469                extern "RustQt" {
470                    #[qobject]
471                    type MyObjectA = super::MyObjectARust;
472
473                    #[qobject]
474                    #[namespace = "type_namespace"]
475                    type MyObjectB = super::MyObjectBRust;
476                }
477
478                #[namespace = "extern_namespace"]
479                extern "RustQt" {
480                    #[qobject]
481                    type MyObjectC = super::MyObjectCRust;
482                }
483            }
484        };
485        let parser = Parser::from(module).unwrap();
486        assert_eq!(parser.type_names.num_types(), 22);
487        assert_eq!(
488            parser
489                .type_names
490                .namespace(&format_ident!("MyObjectA"))
491                .unwrap()
492                .unwrap(),
493            "bridge_namespace"
494        );
495        assert_eq!(
496            parser
497                .type_names
498                .namespace(&format_ident!("MyObjectB"))
499                .unwrap()
500                .unwrap(),
501            "type_namespace"
502        );
503        assert_eq!(
504            parser
505                .type_names
506                .namespace(&format_ident!("MyObjectC"))
507                .unwrap()
508                .unwrap(),
509            "extern_namespace"
510        );
511
512        assert_eq!(
513            parser
514                .type_names
515                .namespace(&format_ident!("MyObjectARust"))
516                .unwrap(),
517            None
518        );
519        assert_eq!(
520            parser
521                .type_names
522                .namespace(&format_ident!("MyObjectBRust"))
523                .unwrap(),
524            None
525        );
526        assert_eq!(
527            parser
528                .type_names
529                .namespace(&format_ident!("MyObjectCRust"))
530                .unwrap(),
531            None
532        );
533    }
534}