Skip to content
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

Copy docs to getter in Properties macro #1490

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions examples/object_subclass/author.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,20 @@ 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<String>,
/// # 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<String>,
}
Expand Down
15 changes: 15 additions & 0 deletions glib-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)]`.
/// The trait name is optional, and defaults to `MyTypePropertiesExt`, where `MyType` is extracted from the wrapper type.
Expand Down Expand Up @@ -1407,7 +1411,9 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream {
/// pub struct Foo {
/// #[property(get, set = Self::set_fizz)]
/// fizz: RefCell<String>,
/// /// 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<Author>,
/// #[property(get, set, explicit_notify, lax_validation)]
Expand All @@ -1420,6 +1426,15 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream {
/// optional: RefCell<Option<String>>,
/// #[property(get, set)]
/// smart_pointer: Rc<RefCell<String>>,
/// /// # 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<bool>,
/// }
///
/// #[glib::derived_properties]
Expand Down
105 changes: 84 additions & 21 deletions glib-macros/src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -255,6 +255,7 @@ struct PropDesc {
field_ident: syn::Ident,
ty: syn::Type,
name: syn::LitStr,
comments: Vec<Attribute>,
override_class: Option<syn::Type>,
override_interface: Option<syn::Type>,
nullable: bool,
Expand All @@ -271,6 +272,7 @@ impl PropDesc {
attrs_span: proc_macro2::Span,
field_ident: syn::Ident,
field_ty: syn::Type,
comments: Vec<Attribute>,
attrs: ReceivedAttrs,
) -> syn::Result<Self> {
let ReceivedAttrs {
Expand Down Expand Up @@ -321,6 +323,7 @@ impl PropDesc {
field_ident,
ty,
name,
comments,
override_class,
override_interface,
nullable,
Expand Down Expand Up @@ -524,26 +527,33 @@ fn expand_set_property_fn(props: &[PropDesc]) -> TokenStream2 {
}

fn parse_fields(fields: syn::Fields) -> syn::Result<Vec<PropDesc>> {
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::<syn::Result<_>>()
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<Attribute> = 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
Expand All @@ -559,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" => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would add Notify support as well while at it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense yeah. Though I think it'll have to be in a different function because we call this one from within the loop inside expand_impl_getset_properties and the notify code is generated from a different one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added # Notify to copy the docs into notify_$property but now I'm not sure if that's what you mean or if it was supposed to be the connect_$property_notify as this one is maybe more likely to be something that a user of a class would need documentation about when that triggers or what the data would mean there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would auto-generate the connect_property_notify docs based on the notify one but not sure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How often does one want to document the notify_$property method itself? Wouldn't that always just say to notify the property? I suppose you could mention when to call it as a user of the class, but I don't know how common that is for public types.

But the connect_$property_notify method I think would benefit much more commonly from docs, no? You might want to specify non-obvious times it would get called, maybe values that depend on other values?

I don't have experience with maintaining a public package, but looking at the docs for gtk itself, e.g. https://docs.gtk.org/gdk4/class.Surface.html (not that we have to match C but consistency is nice) the docs for the signals seem like the connect_ side of the signals rather than the emitting side. Maybe it should be a # Signal heading instead? That would also make it match tighter if we end up making it so there's some sort of list of signals in the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like I was conflating explicitly-defined signals and property change notifications, as the latter is not spelled out in the docs, so now I'm not sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because C doesn't generate a "safe" wrapper for signals compared to Rust bindings that is why there is no documentation for anything. Even less for the notify signal.

I would say, let us just skip having a special header for both Notify and the connect_notify and instead manually amend something like "Notify changes for property X" and "Listen to changes of property X".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've removed the handling for notify and added generic comments that just mention the notification name. We can always add some more "instructions" to the comment if this is something people want to do.

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<syn::ImplItemFn> {
let crate_ident = crate_ident_new();
let defs = props.iter().filter(|p| !p.is_overriding()).map(|p| {
Expand All @@ -567,9 +621,12 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec<syn::ImplItemFn> {
let ident = name_to_ident(name);
let ty = &p.ty;

let (getter_docs, setter_docs) = arrange_property_comments(&p.comments);

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 {
Expand Down Expand Up @@ -597,12 +654,14 @@ fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec<syn::ImplItemFn> {
};
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))
}
)
});

[getter, setter]
});
defs.flatten() // flattens []
Expand All @@ -617,7 +676,9 @@ fn expand_impl_connect_prop_notify(props: &[PropDesc]) -> Vec<syn::ImplItemFn> {
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<F: Fn(&Self) + 'static>(&self, f: F) -> #crate_ident::SignalHandlerId {
self.connect_notify_local(::core::option::Option::Some(#stripped_name), move |this, _| {
Expand All @@ -636,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(
Expand Down
Loading