From 52709ebca0ed91b1a71310170a1074a776370ea1 Mon Sep 17 00:00:00 2001 From: Tim Yuen Date: Wed, 6 Sep 2023 14:50:39 -0400 Subject: [PATCH 1/2] add #[property(...)] macro for adding gdscript properties not associated with a struct field, add tests for #[property(...)] macro, add parse_many to KvParser --- godot-macros/src/class/data_models/field.rs | 12 ++- .../src/class/data_models/field_var.rs | 9 +- godot-macros/src/class/derive_godot_class.rs | 42 +++++++++- godot-macros/src/lib.rs | 32 +++++++- godot-macros/src/util/kv_parser.rs | 18 ++++ itest/godot/ManualFfiTests.gd | 19 +++++ itest/rust/src/object_tests/property_test.rs | 82 ++++++++++++++++++- 7 files changed, 205 insertions(+), 9 deletions(-) diff --git a/godot-macros/src/class/data_models/field.rs b/godot-macros/src/class/data_models/field.rs index 170565b90..a9dea2a42 100644 --- a/godot-macros/src/class/data_models/field.rs +++ b/godot-macros/src/class/data_models/field.rs @@ -16,7 +16,17 @@ pub struct Field { } impl Field { - pub fn new(field: &venial::NamedField) -> Self { + pub fn new(name: Ident, ty: venial::TyExpr) -> Self { + Self { + name, + ty, + default: None, + var: None, + export: None, + } + } + + pub fn from_named_field(field: &venial::NamedField) -> Self { Self { name: field.name.clone(), ty: field.ty.clone(), diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 749a48a1d..f6412d2d2 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -87,7 +87,7 @@ pub enum GetterSetter { } impl GetterSetter { - pub(super) fn parse(parser: &mut KvParser, key: &str) -> ParseResult { + pub(crate) fn parse(parser: &mut KvParser, key: &str) -> ParseResult { let getter_setter = match parser.handle_any(key) { // No `get` argument None => GetterSetter::Omitted, @@ -126,6 +126,10 @@ impl GetterSetter { pub fn is_omitted(&self) -> bool { matches!(self, GetterSetter::Omitted) } + + pub fn is_generated(&self) -> bool { + matches!(self, GetterSetter::Generated) + } } /// Used to determine whether a [`GetterSetter`] is supposed to be a getter or setter. @@ -205,7 +209,8 @@ impl GetterSetterImpl { } } - fn from_custom_impl(function_name: &Ident) -> Self { + /// Create a getters/setter impl from a user-defined function. + pub(super) fn from_custom_impl(function_name: &Ident) -> Self { Self { function_name: function_name.clone(), function_impl: TokenStream::new(), diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index b5efb043f..93ed254f1 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use proc_macro2::{Ident, Punct, TokenStream}; +use proc_macro2::{Delimiter, Group, Ident, Punct, TokenStream, TokenTree}; use quote::{format_ident, quote}; use venial::{Declaration, NamedField, Struct, StructFields}; @@ -18,7 +18,8 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult { .ok_or_else(|| venial::Error::new("Not a valid struct"))?; let struct_cfg = parse_struct_attributes(class)?; - let fields = parse_fields(class)?; + let mut fields = parse_fields(class)?; + fields.all_fields.extend(struct_cfg.standlone_properties); let class_name = &class.name; let class_name_str = class.name.to_string(); @@ -89,6 +90,7 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult { let mut base_ty = ident("RefCounted"); let mut has_generated_init = false; let mut is_tool = false; + let mut standlone_properties = vec![]; // #[class] attribute on struct if let Some(mut parser) = KvParser::parse(&class.attributes, "class")? { @@ -107,10 +109,43 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult { parser.finish()?; } + // #[property] attributes on struct + for mut parser in KvParser::parse_many(&class.attributes, "property")? { + let name = parser.handle_expr_required("name")?.to_string(); + let ty = parser.handle_expr_required("type")?; + + let field_var = FieldVar::new_from_kv(&mut parser)?; + if field_var.getter.is_omitted() && field_var.setter.is_omitted() { + bail!( + parser.span(), + "#[property] must define at least 1 getter or setter" + )?; + } + if field_var.getter.is_generated() || field_var.setter.is_generated() { + bail!( + parser.span(), + "#[property] does not support generated getters and setters" + )?; + } + + let mut field = Field::new( + ident(name.as_str()), + venial::TyExpr { + tokens: vec![TokenTree::Group(Group::new(Delimiter::None, ty))], + }, + ); + field.var = Some(field_var); + + standlone_properties.push(field); + + parser.finish()?; + } + Ok(ClassAttributes { base_ty, has_generated_init, is_tool, + standlone_properties, }) } @@ -133,7 +168,7 @@ fn parse_fields(class: &Struct) -> ParseResult { // Attributes on struct fields for (named_field, _punct) in named_fields { let mut is_base = false; - let mut field = Field::new(&named_field); + let mut field = Field::from_named_field(&named_field); // #[base] if let Some(parser) = KvParser::parse(&named_field.attributes, "base")? { @@ -190,6 +225,7 @@ struct ClassAttributes { base_ty: Ident, has_generated_init: bool, is_tool: bool, + standlone_properties: Vec, } fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream { diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 9c2d7e96e..68cc0f2dd 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -302,6 +302,33 @@ use crate::util::ident; /// impl MyStruct {} /// ``` /// +/// Alternatively, a property can be manually registered with Godot. This can be useful +/// when accessing nested structs or when a custom property name is needed. Automatically +/// generated getters/setters are not supported in this attribute. Custom property hints, +/// hint strings, and usage flags are supported. +/// ``` +/// use godot::prelude::*; +/// +/// #[derive(GodotClass)] +/// // Registers `my_field` as `my_int` with a getter and setter +/// #[property(name = my_int, type = i64, get = get_my_field, set = set_my_field)] +/// struct MyStruct { +/// my_field: i64, +/// } +/// +/// #[godot_api] +/// impl MyStruct { +/// #[func] +/// pub fn get_my_field(&self) -> i64 { +/// self.my_field +/// } +/// +/// #[func] +/// pub fn set_my_field(&mut self, value: i64) { +/// self.my_field = value; +/// } +/// } +/// ``` /// /// # Signals /// @@ -318,7 +345,10 @@ use crate::util::ident; /// for more information and further customization. /// /// This is very similar to [GDScript's `@tool` feature](https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html). -#[proc_macro_derive(GodotClass, attributes(class, base, var, export, init, signal))] +#[proc_macro_derive( + GodotClass, + attributes(class, base, var, export, init, signal, property) +)] pub fn derive_godot_class(input: TokenStream) -> TokenStream { translate(input, class::derive_godot_class) } diff --git a/godot-macros/src/util/kv_parser.rs b/godot-macros/src/util/kv_parser.rs index 6b7b273e7..825c4c384 100644 --- a/godot-macros/src/util/kv_parser.rs +++ b/godot-macros/src/util/kv_parser.rs @@ -59,6 +59,24 @@ impl KvParser { Ok(found_attr) } + /// Create many new parsers which check for the presence of many `#[expected]` attributes. + pub fn parse_many(attributes: &[Attribute], expected: &str) -> ParseResult> { + let mut found_attrs = vec![]; + + for attr in attributes.iter() { + let path = &attr.path; + if path_is_single(path, expected) { + let attr_name = expected.to_string(); + found_attrs.push(Self { + span: attr.tk_brackets.span, + map: ParserState::parse(attr_name, &attr.value)?, + }); + } + } + + Ok(found_attrs) + } + pub fn span(&self) -> Span { self.span } diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index 9563cad44..8f7efb13e 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -304,3 +304,22 @@ func test_func_rename(): assert_eq(func_rename.has_method("renamed_static"), false) assert_eq(func_rename.has_method("spell_static"), true) assert_eq(func_rename.spell_static(), "static") + +func test_standalone_property(): + var standalone_property := StandaloneProperty.new() + + assert_eq(standalone_property.my_int, 0) + assert_eq(standalone_property.readonly_int, 0) + assert_eq(standalone_property.int_array, [0]) + + standalone_property.my_int = 2 + + assert_eq(standalone_property.my_int, 2) + assert_eq(standalone_property.readonly_int, 2) + assert_eq(standalone_property.int_array, [2]) + + standalone_property.int_array = [10, 11] + + assert_eq(standalone_property.my_int, 10) + assert_eq(standalone_property.readonly_int, 10) + assert_eq(standalone_property.int_array, [10]) diff --git a/itest/rust/src/object_tests/property_test.rs b/itest/rust/src/object_tests/property_test.rs index 01f28322c..07813c521 100644 --- a/itest/rust/src/object_tests/property_test.rs +++ b/itest/rust/src/object_tests/property_test.rs @@ -11,8 +11,6 @@ use godot::{ test::itest, }; -// No tests currently, tests using these classes are in Godot scripts. - #[derive(GodotClass)] #[class(base=Node)] struct HasProperty { @@ -368,3 +366,83 @@ fn derive_export() { "A:0,B:1,C:2".to_variant() ); } + +#[derive(GodotClass)] +#[property(name = my_int, type = i32, get = get_integer, set = set_integer)] +#[property(name = readonly_int, type = i32, get = get_integer)] +#[property(name = int_array, type = Array, get = get_integer_as_array, set = set_integer_from_array_front)] +pub struct StandaloneProperty { + integer: i32, +} + +#[godot_api] +impl RefCountedVirtual for StandaloneProperty { + fn init(_base: godot::obj::Base) -> Self { + Self { integer: 0 } + } +} + +#[godot_api] +impl StandaloneProperty { + #[func] + fn get_integer(&self) -> i32 { + self.integer + } + + #[func] + fn set_integer(&mut self, integer: i32) { + self.integer = integer; + } + + #[func] + fn get_integer_as_array(&self) -> Array { + let mut a = Array::new(); + a.push(self.integer); + + a + } + + #[func] + fn set_integer_from_array_front(&mut self, array: VariantArray) { + if let Some(v) = array.first() { + if let Ok(v) = v.try_to::() { + self.integer = v; + } + } + } +} + +#[itest] +fn standalone_property() { + let class: Gd = Gd::new_default(); + + let property = class + .get_property_list() + .iter_shared() + .find(|c| c.get_or_nil("name") == "my_int".to_variant()) + .unwrap(); + + assert_eq!( + property.get_or_nil("class_name"), + "StandaloneProperty".to_variant() + ); + assert_eq!( + property.get_or_nil("type"), + (VariantType::Int as i32).to_variant() + ); + + let property = class + .get_property_list() + .iter_shared() + .find(|c| c.get_or_nil("name") == "readonly_int".to_variant()) + .unwrap(); + + assert_eq!( + property.get_or_nil("class_name"), + "StandaloneProperty".to_variant() + ); + assert_eq!( + property.get_or_nil("type"), + (VariantType::Int as i32).to_variant() + ); +} From 46afcc7fc5b86454ba8ea4db4d6fa39a63fd565e Mon Sep 17 00:00:00 2001 From: Tim Yuen Date: Fri, 8 Sep 2023 02:25:14 -0400 Subject: [PATCH 2/2] add alternative syntax for manual property registration --- godot-macros/src/class/derive_godot_class.rs | 38 ++++++++++++++++++++ godot-macros/src/util/kv_parser.rs | 30 ++++++++++++++-- godot-macros/src/util/list_parser.rs | 16 +++++++-- itest/godot/ManualFfiTests.gd | 3 ++ itest/rust/src/object_tests/property_test.rs | 6 ++++ 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 93ed254f1..3a422500b 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -106,6 +106,44 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult { is_tool = true; } + if let Some(mut list_parser) = parser.handle_array("properties")? { + while let Some(inner_list_parser) = list_parser.try_next_list(Delimiter::Parenthesis)? { + let mut parser = + KvParser::parse_from_list_parser(inner_list_parser, Delimiter::Parenthesis)?; + + let name = parser.handle_expr_required("name")?.to_string(); + let ty = parser.handle_expr_required("type")?; + + let field_var = FieldVar::new_from_kv(&mut parser)?; + if field_var.getter.is_omitted() && field_var.setter.is_omitted() { + bail!( + parser.span(), + "#[properties] item must define at least 1 getter or setter" + )?; + } + if field_var.getter.is_generated() || field_var.setter.is_generated() { + bail!( + parser.span(), + "#[properties] item does not support generated getters and setters" + )?; + } + + let mut field = Field::new( + ident(name.as_str()), + venial::TyExpr { + tokens: vec![TokenTree::Group(Group::new(Delimiter::None, ty))], + }, + ); + field.var = Some(field_var); + + standlone_properties.push(field); + + parser.finish()?; + } + + list_parser.finish()?; + } + parser.finish()?; } diff --git a/godot-macros/src/util/kv_parser.rs b/godot-macros/src/util/kv_parser.rs index 825c4c384..5fc7ec205 100644 --- a/godot-macros/src/util/kv_parser.rs +++ b/godot-macros/src/util/kv_parser.rs @@ -5,7 +5,7 @@ */ use crate::ParseResult; -use proc_macro2::{Delimiter, Ident, Spacing, Span, TokenStream, TokenTree}; +use proc_macro2::{Delimiter, Ident, Punct, Spacing, Span, TokenStream, TokenTree}; use quote::ToTokens; use std::collections::HashMap; use venial::Attribute; @@ -77,6 +77,28 @@ impl KvParser { Ok(found_attrs) } + pub fn parse_from_list_parser(parser: ListParser, delimiter: Delimiter) -> ParseResult { + let span = parser.span_close; + let tokens = parser + .lists + .into_iter() + .flat_map(|v| { + let mut tokens = v.into_tokens(); + tokens.push(TokenTree::Punct(Punct::new(',', Spacing::Alone))); + + tokens + }) + .collect::>(); + + Ok(Self { + span, + map: ParserState::parse( + "nested list".to_string(), + &venial::AttributeValue::Group(venial::GroupSpan { delimiter, span }, tokens), + )?, + }) + } + pub fn span(&self) -> Span { self.span } @@ -333,7 +355,11 @@ impl<'a> ParserState<'a> { } else { "".to_owned() }; - return bail!(cur, "expected identifier{parens_hint}"); + let attr_name = self.attr_name; + let tokens = self.tokens; + let prev = self.prev; + let cur = self.cur; + return bail!(cur, "expected identifier{parens_hint} attr {attr_name} tokens {tokens:?} prev {prev:?} cur {cur:?}"); } } } diff --git a/godot-macros/src/util/list_parser.rs b/godot-macros/src/util/list_parser.rs index 9f7866fe2..5c00fd7b7 100644 --- a/godot-macros/src/util/list_parser.rs +++ b/godot-macros/src/util/list_parser.rs @@ -15,9 +15,9 @@ use crate::ParseResult; /// Parses a list of tokens as an ordered list of values. Unlike [`KvParser`] which treats the tokens as a /// set of values. pub struct ListParser { - lists: VecDeque, + pub(super) lists: VecDeque, /// The last span of the list, usually a closing parenthesis. - span_close: Span, + pub(super) span_close: Span, } impl ListParser { @@ -194,6 +194,18 @@ impl ListParser { } } + /// Takes the next list element of the list. + /// + /// # Example + /// `((name = foo), (name = boo))` will yield `(name = foo)` + pub(crate) fn try_next_list(&mut self, delimiter: Delimiter) -> ParseResult> { + let Some(kv) = self.pop_next() else { + return Ok(None); + }; + + Ok(Some(ListParser::new_from_tree(kv.single()?, delimiter)?)) + } + /// Ensure all values have been consumed. pub fn finish(&mut self) -> ParseResult<()> { if let Some(kv) = self.pop_next() { diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index 8f7efb13e..3ee983334 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -323,3 +323,6 @@ func test_standalone_property(): assert_eq(standalone_property.my_int, 10) assert_eq(standalone_property.readonly_int, 10) assert_eq(standalone_property.int_array, [10]) + + assert_eq(standalone_property.first_int, 10) + assert_eq(standalone_property.second_int, 10) diff --git a/itest/rust/src/object_tests/property_test.rs b/itest/rust/src/object_tests/property_test.rs index 07813c521..dc13ac854 100644 --- a/itest/rust/src/object_tests/property_test.rs +++ b/itest/rust/src/object_tests/property_test.rs @@ -368,6 +368,12 @@ fn derive_export() { } #[derive(GodotClass)] +#[class( + properties = [ + (name = first_int, type = i32, get = get_integer), + (name = second_int, type = i32, get = get_integer) + ] +)] #[property(name = my_int, type = i32, get = get_integer, set = set_integer)] #[property(name = readonly_int, type = i32, get = get_integer)] #[property(name = int_array, type = Array, get = get_integer_as_array, set = set_integer_from_array_front)]