From 25f280070d21869a5731b673fd532af3a0ac34a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 26 Aug 2024 19:55:24 +0200 Subject: [PATCH 1/6] examples: add some docs to the example using the Properties macro These will be used later on by the macro. --- examples/object_subclass/author.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/object_subclass/author.rs b/examples/object_subclass/author.rs index 86b6001149b5..496108a2cdbf 100644 --- a/examples/object_subclass/author.rs +++ b/examples/object_subclass/author.rs @@ -14,7 +14,12 @@ mod imp { #[derive(Properties, Default)] #[properties(wrapper_type = super::Author)] pub struct Author { + /// The name of the author + /// + /// Just their given name, not their surname. #[property(get, set)] + /// A helpful name-surname combination. + #[property(name = "name-surname", get = |author: &Self| format!("{} {}", author.name.borrow(), author.surname.borrow()))] name: RefCell, #[property(get, set)] surname: RefCell, From 97be519c08c160b0c458761cec5e0a94055aec63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 26 Aug 2024 19:59:40 +0200 Subject: [PATCH 2/6] glib-macros: Properties: copy property docs to getter We take the docs directly preceding a `#[property]` and copy them into the generated getter method. --- glib-macros/src/lib.rs | 6 ++++ glib-macros/src/properties.rs | 60 ++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/glib-macros/src/lib.rs b/glib-macros/src/lib.rs index 8f66dc855fd5..49e7a6473894 100644 --- a/glib-macros/src/lib.rs +++ b/glib-macros/src/lib.rs @@ -1344,6 +1344,10 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream { /// * `connect_$property_notify()` /// * `notify_$property()` /// +/// # Documentation +/// +/// Doc comments preceding a `#[property]` attribute will be copied to the generated getter method. +/// /// ## Extension trait /// You can choose to move the method definitions to a trait by using `#[properties(wrapper_type = super::MyType, ext_trait = MyTypePropertiesExt)]`. /// The trait name is optional, and defaults to `MyTypePropertiesExt`, where `MyType` is extracted from the wrapper type. @@ -1407,7 +1411,9 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream { /// pub struct Foo { /// #[property(get, set = Self::set_fizz)] /// fizz: RefCell, +/// /// The author's name /// #[property(name = "author-name", get, set, type = String, member = name)] +/// /// The author's childhood nickname /// #[property(name = "author-nick", get, set, type = String, member = nick)] /// author: RefCell, /// #[property(get, set, explicit_notify, lax_validation)] diff --git a/glib-macros/src/properties.rs b/glib-macros/src/properties.rs index 35ba2f7ccc4d..822232129391 100644 --- a/glib-macros/src/properties.rs +++ b/glib-macros/src/properties.rs @@ -12,7 +12,7 @@ use syn::parse::Parse; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::Token; -use syn::{parse_quote_spanned, LitStr}; +use syn::{parse_quote_spanned, Attribute, LitStr}; pub struct PropsMacroInput { wrapper_ty: syn::Path, @@ -255,6 +255,7 @@ struct PropDesc { field_ident: syn::Ident, ty: syn::Type, name: syn::LitStr, + comments: Vec, override_class: Option, override_interface: Option, nullable: bool, @@ -271,6 +272,7 @@ impl PropDesc { attrs_span: proc_macro2::Span, field_ident: syn::Ident, field_ty: syn::Type, + comments: Vec, attrs: ReceivedAttrs, ) -> syn::Result { let ReceivedAttrs { @@ -321,6 +323,7 @@ impl PropDesc { field_ident, ty, name, + comments, override_class, override_interface, nullable, @@ -524,26 +527,33 @@ fn expand_set_property_fn(props: &[PropDesc]) -> TokenStream2 { } fn parse_fields(fields: syn::Fields) -> syn::Result> { - fields - .into_iter() - .flat_map(|field| { - let syn::Field { - ident, attrs, ty, .. - } = field; - attrs - .into_iter() - .filter(|a| a.path().is_ident("property")) - .map(move |prop_attrs| { - let span = prop_attrs.span(); - PropDesc::new( - span, - ident.as_ref().unwrap().clone(), - ty.clone(), - prop_attrs.parse_args()?, - ) - }) - }) - .collect::>() + let mut properties = vec![]; + + for field in fields.into_iter() { + let syn::Field { + ident, attrs, ty, .. + } = field; + // Store the comments until the next `#[property]` we see and then attach them to it. + let mut comments: Vec = vec![]; + for prop_attr in attrs.iter() { + if prop_attr.path().is_ident("doc") { + comments.push(prop_attr.clone()); + } else if prop_attr.path().is_ident("property") { + let span = prop_attr.span(); + let existing_comments = comments; + comments = vec![]; + properties.push(PropDesc::new( + span, + ident.as_ref().unwrap().clone(), + ty.clone(), + existing_comments, + prop_attr.parse_args()?, + )?); + } + } + } + + Ok(properties) } /// Converts a glib property name to a correct rust ident @@ -567,7 +577,7 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { let ident = name_to_ident(name); let ty = &p.ty; - let getter = p.get.is_some().then(|| { + let mut getter: Option = p.get.is_some().then(|| { let span = p.attrs_span; parse_quote_spanned!(span=> #[must_use] @@ -578,6 +588,12 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { ) }); + if let Some(ref mut getter) = getter { + for lit in &p.comments { + getter.attrs.push(lit.clone()); + } + } + let setter = (p.set.is_some() && !p.is_construct_only).then(|| { let ident = format_ident!("set_{}", ident); let target_ty = quote!(<<#ty as #crate_ident::property::Property>::Value as #crate_ident::prelude::HasParamSpec>::SetValue); From 60f69d1092c5b2ae1b8e52e10f20d9cf87f4ec5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Fri, 20 Dec 2024 23:42:51 +0000 Subject: [PATCH 3/6] glib-macros: Properties: allow setting getter and setter docs In the previous commit we made it so we copy the docs for a `#[property]` in to the getter. This was an improvement but it still did not allow for any doc comments on the setter that might be relevant only there. Here we introduce the ability to specify doc comments for the getter and setter individually via the use of the header syntax to switch between them. By writing `# Getter` or `# Setter` in a line, you make the lines following it go to the getter or setter function definitions. If neither of these values exist, we copy the doc comments to both. If you use these values, then anything in the doc comments before the first one is discarded. --- examples/object_subclass/author.rs | 7 ++++ glib-macros/src/properties.rs | 59 ++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/examples/object_subclass/author.rs b/examples/object_subclass/author.rs index 496108a2cdbf..be6e3bcc8f5a 100644 --- a/examples/object_subclass/author.rs +++ b/examples/object_subclass/author.rs @@ -21,6 +21,13 @@ mod imp { /// A helpful name-surname combination. #[property(name = "name-surname", get = |author: &Self| format!("{} {}", author.name.borrow(), author.surname.borrow()))] name: RefCell, + /// # Getter + /// + /// This is how you can get the surname of the author. + /// + /// # Setter + /// + /// You can change the surname of the author too if you want. #[property(get, set)] surname: RefCell, } diff --git a/glib-macros/src/properties.rs b/glib-macros/src/properties.rs index 822232129391..18f473f1da93 100644 --- a/glib-macros/src/properties.rs +++ b/glib-macros/src/properties.rs @@ -569,6 +569,50 @@ fn strip_raw_prefix_from_name(name: &LitStr) -> LitStr { ) } +/// Splits the comments for a property between the getter and setter +/// +/// The return tuple is the attributes to copy over into the getter and setter +/// respectively. +fn arrange_property_comments(comments: &[Attribute]) -> (Vec<&Attribute>, Vec<&Attribute>) { + let mut untagged = vec![]; + let mut getter = vec![]; + let mut setter = vec![]; + let mut saw_section = false; + + // We start with no tags so if the programmer doesn't split the comments we can still arrange them. + let mut current_section = &mut untagged; + for attr in comments { + if let syn::Meta::NameValue(meta) = &attr.meta { + if let syn::Expr::Lit(expr) = &meta.value { + if let syn::Lit::Str(lit_str) = &expr.lit { + // Now that we have the one line of comment, see if we need + // to switch a particular section to be the active one (via + // the header syntax) or add the current line to the active + // section. + match lit_str.value().trim() { + "# Getter" => { + current_section = &mut getter; + saw_section = true; + } + "# Setter" => { + current_section = &mut setter; + saw_section = true; + } + _ => current_section.push(attr), + } + } + } + } + } + + // If no sections were defined then we put the same in both + if !saw_section { + return (untagged.clone(), untagged); + } + + (getter, setter) +} + fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { let crate_ident = crate_ident_new(); let defs = props.iter().filter(|p| !p.is_overriding()).map(|p| { @@ -577,6 +621,8 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { let ident = name_to_ident(name); let ty = &p.ty; + let (getter_docs, setter_docs) = arrange_property_comments(&p.comments); + let mut getter: Option = p.get.is_some().then(|| { let span = p.attrs_span; parse_quote_spanned!(span=> @@ -589,12 +635,12 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { }); if let Some(ref mut getter) = getter { - for lit in &p.comments { - getter.attrs.push(lit.clone()); + for attr in getter_docs { + getter.attrs.push(attr.clone()); } } - let setter = (p.set.is_some() && !p.is_construct_only).then(|| { + let mut setter: Option = (p.set.is_some() && !p.is_construct_only).then(|| { let ident = format_ident!("set_{}", ident); let target_ty = quote!(<<#ty as #crate_ident::property::Property>::Value as #crate_ident::prelude::HasParamSpec>::SetValue); let set_ty = if p.nullable { @@ -619,6 +665,13 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { } ) }); + + if let Some(ref mut setter) = setter { + for attr in setter_docs { + setter.attrs.push(attr.clone()); + } + } + [getter, setter] }); defs.flatten() // flattens [] From bf869503c94157ab930ddaad8d40474b052f05d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Sat, 21 Dec 2024 00:00:37 +0000 Subject: [PATCH 4/6] glib-macros: Properties: copy over the doc comments more elegantly Let's copy the comments as part of the `parse_quote_spanned!` when we create the rest of the tokens for the code instead of adding it after the fact. --- glib-macros/src/properties.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/glib-macros/src/properties.rs b/glib-macros/src/properties.rs index 18f473f1da93..51a4c81671ef 100644 --- a/glib-macros/src/properties.rs +++ b/glib-macros/src/properties.rs @@ -623,9 +623,10 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { let (getter_docs, setter_docs) = arrange_property_comments(&p.comments); - let mut getter: Option = p.get.is_some().then(|| { + let getter = p.get.is_some().then(|| { let span = p.attrs_span; parse_quote_spanned!(span=> + #(#getter_docs)* #[must_use] #[allow(dead_code)] pub fn #ident(&self) -> <#ty as #crate_ident::property::Property>::Value { @@ -634,13 +635,7 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { ) }); - if let Some(ref mut getter) = getter { - for attr in getter_docs { - getter.attrs.push(attr.clone()); - } - } - - let mut setter: Option = (p.set.is_some() && !p.is_construct_only).then(|| { + let setter = (p.set.is_some() && !p.is_construct_only).then(|| { let ident = format_ident!("set_{}", ident); let target_ty = quote!(<<#ty as #crate_ident::property::Property>::Value as #crate_ident::prelude::HasParamSpec>::SetValue); let set_ty = if p.nullable { @@ -659,6 +654,7 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { }; let span = p.attrs_span; parse_quote_spanned!(span=> + #(#setter_docs)* #[allow(dead_code)] pub fn #ident<'a>(&self, value: #set_ty) { self.set_property_from_value(#stripped_name, &::std::convert::From::from(#upcasted_borrowed_value)) @@ -666,12 +662,6 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec { ) }); - if let Some(ref mut setter) = setter { - for attr in setter_docs { - setter.attrs.push(attr.clone()); - } - } - [getter, setter] }); defs.flatten() // flattens [] From 3581e84c6d5bf0301aa536051e6e120763dd1c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Mon, 17 Feb 2025 22:40:45 +0100 Subject: [PATCH 5/6] glib-macros: extend `Properties` docs with the getter-setter docs --- glib-macros/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/glib-macros/src/lib.rs b/glib-macros/src/lib.rs index 49e7a6473894..ca6328b48aaa 100644 --- a/glib-macros/src/lib.rs +++ b/glib-macros/src/lib.rs @@ -1346,7 +1346,7 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream { /// /// # Documentation /// -/// Doc comments preceding a `#[property]` attribute will be copied to the generated getter method. +/// Doc comments preceding a `#[property]` attribute will be copied to the generated getter and setter methods. You can specify different comments by the getter and setter by using `# Getter` and `# Setter` headings. The text under the header will be copied to the respective method. /// /// ## Extension trait /// You can choose to move the method definitions to a trait by using `#[properties(wrapper_type = super::MyType, ext_trait = MyTypePropertiesExt)]`. @@ -1426,6 +1426,15 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream { /// optional: RefCell>, /// #[property(get, set)] /// smart_pointer: Rc>, +/// /// # Getter +/// /// +/// /// Get the value of the property `extra_comments` +/// /// +/// /// # Setter +/// /// +/// /// This is the comment for the setter of the `extra_comments` field. +/// #[property(get, set)] +/// extra_comments: RefCell, /// } /// /// #[glib::derived_properties] From f3799c23808642763c946d2c39155714d1820b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 19 Feb 2025 08:57:00 +0100 Subject: [PATCH 6/6] glib-macros: add some generic comments for property notifications --- glib-macros/src/properties.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/glib-macros/src/properties.rs b/glib-macros/src/properties.rs index 51a4c81671ef..c2743290dfcd 100644 --- a/glib-macros/src/properties.rs +++ b/glib-macros/src/properties.rs @@ -676,7 +676,9 @@ fn expand_impl_connect_prop_notify(props: &[PropDesc]) -> Vec { let stripped_name = strip_raw_prefix_from_name(name); let fn_ident = format_ident!("connect_{}_notify", name_to_ident(name)); let span = p.attrs_span; + let doc = format!("Listen for notifications of a change in the `{}` property", name.value()); parse_quote_spanned!(span=> + #[doc = #doc] #[allow(dead_code)] pub fn #fn_ident(&self, f: F) -> #crate_ident::SignalHandlerId { self.connect_notify_local(::core::option::Option::Some(#stripped_name), move |this, _| { @@ -695,7 +697,9 @@ fn expand_impl_notify_prop(wrapper_type: &syn::Path, props: &[PropDesc]) -> Vec< let fn_ident = format_ident!("notify_{}", name_to_ident(&name)); let span = p.attrs_span; let enum_ident = name_to_enum_ident(name.value()); + let doc = format!("Notify listeners of a change in the `{}` property", name.value()); parse_quote_spanned!(span=> + #[doc = #doc] #[allow(dead_code)] pub fn #fn_ident(&self) { self.notify_by_pspec(