Skip to content

Generic attributes parsing #1301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions crates/cxx-qt-gen/src/parser/attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
// SPDX-FileContributor: Ben Ford <[email protected]>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::{parser, syntax::path::path_compare_str};
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use syn::{spanned::Spanned, Attribute, Error, Result};

#[derive(Clone)]
pub struct ParsedAttributes {
pub cxx_qt_attrs: BTreeMap<String, Vec<Attribute>>,
pub passthrough_attrs: Vec<Attribute>,
}

pub enum AttributeConstraint {
/// Indicates that there must be only one of this attribute
Unique,
/// Indicates there can be multiple of this attribute
Duplicate,
/// Indicates that this attribute is required
Required,
}

/// Iterate the attributes of the method to extract cfg attributes
pub fn extract_cfgs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs
.iter()
.filter(|attr| path_compare_str(attr.meta.path(), &["cfg"]))
.cloned()
.collect()
}

/// Iterate the attributes of the method to extract Doc attributes (doc comments are parsed as this)
pub fn extract_docs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs
.iter()
.filter(|attr| path_compare_str(attr.meta.path(), &["doc"]))
.cloned()
.collect()
}

impl<'a> ParsedAttributes {
/// Collects a Map of all attributes found from the allowed list
/// Will error if an attribute which is not in the allowed list is found, or attribute is used incorrectly
pub fn require_attributes(
mut attrs: Vec<Attribute>,
allowed: &'a [(AttributeConstraint, &str)],
) -> Result<ParsedAttributes> {
let mut output = BTreeMap::<String, Vec<Attribute>>::default();
for attr in attrs.drain(..) {
let index = allowed.iter().position(|(_, string)| {
path_compare_str(attr.meta.path(), &parser::split_path(string))
});
if let Some(index) = index {
match allowed[index].0 {
AttributeConstraint::Unique => {
match output.entry(allowed[index].1.into()) {
Entry::Occupied(_) => return Err(Error::new_spanned(
attr,
"There must be at most one of this attribute on this given item",
)),
Entry::Vacant(entry) => {
entry.insert(vec![attr]);
}
}
}
AttributeConstraint::Duplicate => match output.entry(allowed[index].1.into()) {
Entry::Occupied(mut entry) => {
entry.get_mut().push(attr);
}
Entry::Vacant(entry) => {
entry.insert(vec![attr]);
}
},
AttributeConstraint::Required => {}
}
} else {
return Err(Error::new(
attr.span(),
format!(
"Unsupported attribute! The only attributes allowed on this item are\n{}",
allowed
.iter()
.map(|(_, string)| *string)
.collect::<Vec<_>>()
.join(", ")
),
));
}
}
Ok(Self {
cxx_qt_attrs: output,
passthrough_attrs: Default::default(), // TODO: ATTR Pass the actual docs, cfgs, etc... here
})
}

// TODO: ATTR Can this return references instead?
pub fn extract_docs(&self) -> Vec<Attribute> {
self.cxx_qt_attrs
.values()
.flatten()
.filter(|attr| path_compare_str(attr.meta.path(), &["doc"]))
.map(|attr| (*attr).clone())
.collect()
}

// TODO: ATTR Can this return references instead
pub fn extract_cfgs(&self) -> Vec<Attribute> {
self.cxx_qt_attrs
.values()
.flatten()
.filter(|attr| path_compare_str(attr.meta.path(), &["cfg"]))
.map(|attr| (*attr).clone())
.collect()
}

/// Returns all the attributes stored within the struct
/// TODO: ATTR Can we use this without clone
pub fn clone_attrs(&self) -> Vec<Attribute> {
self.cxx_qt_attrs
.values()
.flatten()
.cloned()
.collect::<Vec<_>>()
}

// Wrapper methods for the internal BTreeMaps
// TODO: Refactor usage to use more specialised methods / rename

/// Search in first the CXX-Qt, and then passthrough attributes by key
pub fn get_one(&self, key: &str) -> Option<&Attribute> {
self.cxx_qt_attrs.get(key)?.first()
}

/// Check if CXX-Qt or passthrough attributes contains a particular key
pub fn contains_key(&self, key: &str) -> bool {
self.cxx_qt_attrs.contains_key(key) // TODO: Check in passthrough too
}
}
42 changes: 31 additions & 11 deletions crates/cxx-qt-gen/src/parser/externcxxqt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::parser::attribute::{AttributeConstraint, ParsedAttributes};
use crate::{
parser::{
externqobject::ParsedExternQObject, require_attributes, signals::ParsedSignal,
CaseConversion,
},
parser::{externqobject::ParsedExternQObject, signals::ParsedSignal, CaseConversion},
syntax::{attribute::attribute_get_path, expr::expr_to_string},
};
use syn::{
Expand Down Expand Up @@ -36,18 +34,20 @@ impl ParsedExternCxxQt {
parent_namespace: Option<&str>,
) -> Result<Self> {
// TODO: support cfg on foreign mod blocks
let attrs = require_attributes(
&foreign_mod.attrs,
&["namespace", "auto_cxx_name", "auto_rust_name"],
let attrs = ParsedAttributes::require_attributes(
foreign_mod.attrs,
&[
(AttributeConstraint::Unique, "namespace"),
(AttributeConstraint::Unique, "auto_cxx_name"),
(AttributeConstraint::Unique, "auto_rust_name"),
],
)?;

let auto_case = CaseConversion::from_attrs(&attrs)?;

let namespace = attrs
.get("namespace")
.map(|attr| -> Result<String> {
expr_to_string(&attr.meta.require_name_value()?.value)
})
.get_one("namespace")
.map(|attr| expr_to_string(&attr.meta.require_name_value()?.value))
.transpose()?;

let mut extern_cxx_block = ParsedExternCxxQt {
Expand Down Expand Up @@ -221,6 +221,26 @@ mod tests {
type QPushButton;
}
}

// Duplicate base attr is an error
{
extern "C++Qt" {
#[base = QPushButton]
#[base = QPushButton]
#[qobject]
type QPushButtonChild;
}
}

// All types in "C++Qt" blocks must be marked as QObjects
{
#[namespace = "My namespace"]
#[namespace = "My other namespace"]
unsafe extern "C++Qt" {
#[qobject]
type QPushButton;
}
}
);
}
}
26 changes: 14 additions & 12 deletions crates/cxx-qt-gen/src/parser/externqobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::naming::Name;
use crate::parser::{parse_base_type, require_attributes, CaseConversion};
use crate::parser::attribute::{AttributeConstraint, ParsedAttributes};
use crate::parser::{parse_base_type, CaseConversion};
use syn::{ForeignItemType, Ident, Result};

/// A representation of a QObject to be generated in an extern C++ block
Expand All @@ -17,29 +18,30 @@ pub struct ParsedExternQObject {
}

impl ParsedExternQObject {
const ALLOWED_ATTRS: [&'static str; 7] = [
"cxx_name",
"rust_name",
"namespace",
"cfg",
"doc",
"qobject",
"base",
const ALLOWED_ATTRS: [(AttributeConstraint, &'static str); 7] = [
(AttributeConstraint::Unique, "cxx_name"),
(AttributeConstraint::Unique, "rust_name"),
(AttributeConstraint::Unique, "namespace"),
(AttributeConstraint::Duplicate, "cfg"),
(AttributeConstraint::Duplicate, "doc"),
(AttributeConstraint::Unique, "qobject"),
(AttributeConstraint::Unique, "base"),
];

pub fn parse(
ty: ForeignItemType,
module_ident: &Ident,
parent_namespace: Option<&str>,
) -> Result<ParsedExternQObject> {
let attributes = require_attributes(&ty.attrs, &Self::ALLOWED_ATTRS)?;
// TODO: ATTR Can this be done without clone
let attrs = ParsedAttributes::require_attributes(ty.attrs.clone(), &Self::ALLOWED_ATTRS)?;

let base_class = parse_base_type(&attributes)?;
let base_class = parse_base_type(&attrs)?;

Ok(Self {
name: Name::from_ident_and_attrs(
&ty.ident,
&ty.attrs,
&attrs.clone_attrs(),
parent_namespace,
Some(module_ident),
CaseConversion::none(),
Expand Down
46 changes: 41 additions & 5 deletions crates/cxx-qt-gen/src/parser/externrustqt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::naming::cpp::err_unsupported_item;
use crate::parser::attribute::{AttributeConstraint, ParsedAttributes};
use crate::parser::inherit::ParsedInheritedMethod;
use crate::parser::method::ParsedMethod;
use crate::parser::qobject::ParsedQObject;
use crate::parser::signals::ParsedSignal;
use crate::parser::{require_attributes, CaseConversion};
use crate::parser::CaseConversion;
use crate::syntax::attribute::attribute_get_path;
use crate::syntax::expr::expr_to_string;
use crate::syntax::foreignmod::ForeignTypeIdentAlias;
Expand Down Expand Up @@ -38,9 +39,13 @@ impl ParsedExternRustQt {
parent_namespace: Option<&str>,
) -> Result<Self> {
// TODO: support cfg on foreign mod blocks
let attrs = require_attributes(
&foreign_mod.attrs,
&["namespace", "auto_cxx_name", "auto_rust_name"],
let attrs = ParsedAttributes::require_attributes(
foreign_mod.attrs,
&[
(AttributeConstraint::Unique, "namespace"),
(AttributeConstraint::Unique, "auto_cxx_name"),
(AttributeConstraint::Unique, "auto_rust_name"),
],
)?;

let auto_case = CaseConversion::from_attrs(&attrs)?;
Expand All @@ -51,7 +56,7 @@ impl ParsedExternRustQt {
};

let namespace = attrs
.get("namespace")
.get_one("namespace")
.map(|attr| expr_to_string(&attr.meta.require_name_value()?.value))
.transpose()?
.or_else(|| parent_namespace.map(String::from));
Expand Down Expand Up @@ -223,6 +228,37 @@ mod tests {
}
}

// Duplicate namespace not allowed
{
#[namespace = "My Namespace"]
#[namespace = "My Other Namespace"]
unsafe extern "RustQt" {
#[qinvokable]
fn invokable(self: &MyObject);
}
}

// Duplicate auto_cxx_name not allowed
{

#[auto_cxx_name]
#[auto_cxx_name]
unsafe extern "RustQt" {
#[qobject]
type MyObject = super::MyObjectRust;
}
}

// Duplicate auto_rust_name not allowed
{
#[auto_rust_name]
#[auto_rust_name]
unsafe extern "RustQt" {
#[qobject]
type MyObject = super::MyObjectRust;
}
}

// Namespaces aren't allowed on qinvokables
{
unsafe extern "RustQt" {
Expand Down
27 changes: 13 additions & 14 deletions crates/cxx-qt-gen/src/parser/inherit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::parser::{
extract_cfgs, extract_docs, method::MethodFields, require_attributes, CaseConversion,
};
use crate::parser::attribute::AttributeConstraint;
use crate::parser::{method::MethodFields, CaseConversion};
use core::ops::Deref;
use quote::format_ident;
use std::ops::DerefMut;
Expand All @@ -22,22 +21,22 @@ pub struct ParsedInheritedMethod {
}

impl ParsedInheritedMethod {
const ALLOWED_ATTRS: [&'static str; 6] = [
"cxx_name",
"rust_name",
"qinvokable",
"doc",
"inherit",
"cfg",
const ALLOWED_ATTRS: [(AttributeConstraint, &'static str); 6] = [
(AttributeConstraint::Unique, "cxx_name"),
(AttributeConstraint::Unique, "rust_name"),
(AttributeConstraint::Unique, "qinvokable"),
(AttributeConstraint::Duplicate, "doc"),
(AttributeConstraint::Unique, "inherit"),
(AttributeConstraint::Duplicate, "cfg"),
];

pub fn parse(method: ForeignItemFn, auto_case: CaseConversion) -> Result<Self> {
require_attributes(&method.attrs, &Self::ALLOWED_ATTRS)?;
let docs = extract_docs(&method.attrs);
let cfgs = extract_cfgs(&method.attrs);
let method_fields = MethodFields::parse(method, auto_case, &Self::ALLOWED_ATTRS)?;

let docs = method_fields.attrs.extract_docs();
let cfgs = method_fields.attrs.extract_cfgs();
Ok(Self {
method_fields: MethodFields::parse(method, auto_case)?,
method_fields,
docs,
cfgs,
})
Expand Down
Loading
Loading