From 1b32155fa5cec79028d315de3ceb17fc972b1f75 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Sun, 26 Mar 2023 14:17:38 +0200 Subject: [PATCH] Make `get` and `set` arguments take identifiers instead of strings Refactors `KvParser` to accept some expressions, as long as they don't contain a comma at the top level outside any pair of `[]`, `()` or `{}`. Also tightens up spans in error messages quite a bit. --- godot-macros/src/derive_godot_class.rs | 50 +-- godot-macros/src/lib.rs | 6 +- godot-macros/src/util.rs | 517 +++++++++++++++---------- itest/rust/src/export_test.rs | 16 +- 4 files changed, 331 insertions(+), 258 deletions(-) diff --git a/godot-macros/src/derive_godot_class.rs b/godot-macros/src/derive_godot_class.rs index 8660c0f2d..055f5dbec 100644 --- a/godot-macros/src/derive_godot_class.rs +++ b/godot-macros/src/derive_godot_class.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::util::{bail, ident, string_lit_contents, KvParser, KvValue}; +use crate::util::{bail, ident, KvParser}; use crate::ParseResult; use proc_macro2::{Ident, Punct, TokenStream}; use quote::{format_ident, quote}; @@ -185,8 +185,8 @@ enum GetterSetter { Omitted, /// Trivial getter/setter should be autogenerated. Generated, - /// Getter/setter is hand-written by the user, and here is its name. - Custom(String), + /// Getter/setter is hand-written by the user, and here is its identifier. + Custom(Ident), } impl GetterSetter { @@ -194,21 +194,12 @@ impl GetterSetter { Ok(match parser.handle_any(key) { // No `get` argument None => GetterSetter::Omitted, - // `get` without value - Some(KvValue::None) => GetterSetter::Generated, - // `get = literal` - Some(KvValue::Lit(name_lit)) => { - let Some(name) = string_lit_contents(&name_lit) else { - return bail(format!("argument to {key} must be a string literal, got: {name_lit}"), parser.span()); - }; - GetterSetter::Custom(name) - } - Some(KvValue::Ident(ident)) => { - return bail( - format!("argument to {key} must be a string, got: {ident}"), - parser.span(), - ); - } + Some(value) => match value { + // `get` without value + None => GetterSetter::Generated, + // `get = expr` + Some(value) => GetterSetter::Custom(value.ident()?), + }, }) } } @@ -216,14 +207,14 @@ impl GetterSetter { #[derive(Clone)] struct ExportHint { hint_type: Ident, - description: String, + description: TokenStream, } impl ExportHint { fn none() -> Self { Self { hint_type: ident("PROPERTY_HINT_NONE"), - description: "".to_string(), + description: quote!(""), } } } @@ -242,7 +233,7 @@ impl ExportedField { .map(|hint_type| { Ok(ExportHint { hint_type, - description: parser.handle_lit_required("hint_desc")?, + description: parser.handle_expr_required("hint_desc")?, }) }) .transpose()?; @@ -316,9 +307,6 @@ fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream { description, } = exported_field.hint.clone().unwrap_or_else(ExportHint::none); - // trims '"' and '\' from both ends of the hint description. - let description = description.trim_matches(|c| c == '\\' || c == '"'); - let getter_name; match &exported_field.getter { GetterSetter::Omitted => { @@ -339,10 +327,9 @@ fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream { ::godot::private::gdext_register_method!(#class_name, #signature); }); } - GetterSetter::Custom(name) => { - getter_name = name.clone(); - let getter_ident = ident(&getter_name); - export_tokens.push(make_existence_check(&getter_ident)); + GetterSetter::Custom(getter_ident) => { + getter_name = getter_ident.to_string(); + export_tokens.push(make_existence_check(getter_ident)); } } @@ -366,10 +353,9 @@ fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream { ::godot::private::gdext_register_method!(#class_name, #signature); }); } - GetterSetter::Custom(name) => { - setter_name = name.clone(); - let setter_ident = ident(&setter_name); - export_tokens.push(make_existence_check(&setter_ident)); + GetterSetter::Custom(setter_ident) => { + setter_name = setter_ident.to_string(); + export_tokens.push(make_existence_check(setter_ident)); } }; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index df65f61db..b56749ad0 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -171,14 +171,14 @@ mod util; /// /// If you want to implement your own getter and/or setter, write those as a function on your Rust /// type, expose it using `#[func]`, and annotate the field with -/// `#[export(get = "...", set = "...")]`: +/// `#[export(get = ..., set = ...)]`: /// /// ``` /// use godot::prelude::*; /// /// #[derive(GodotClass)] /// struct MyStruct { -/// #[export(get = "get_my_field", set = "set_my_field")] +/// #[export(get = get_my_field, set = set_my_field)] /// my_field: i64, /// } /// @@ -206,7 +206,7 @@ mod util; /// #[derive(GodotClass)] /// struct MyStruct { /// // Default getter, custom setter. -/// #[export(get, set = "set_my_field")] +/// #[export(get, set = set_my_field)] /// my_field: i64, /// } /// diff --git a/godot-macros/src/util.rs b/godot-macros/src/util.rs index ad20ac7a9..be770c4b6 100644 --- a/godot-macros/src/util.rs +++ b/godot-macros/src/util.rs @@ -7,7 +7,7 @@ // Note: some code duplication with codegen crate use crate::ParseResult; -use proc_macro2::{Ident, Span, TokenTree}; +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::spanned::Spanned; use quote::{format_ident, ToTokens}; use std::collections::HashMap; @@ -17,17 +17,18 @@ pub fn ident(s: &str) -> Ident { format_ident!("{}", s) } -/// Given a string containing a string literal in Rust syntax, i.e. double quotes inside the -/// string, returns the string represented by that literal. -pub fn string_lit_contents(string_lit: &str) -> Option { - Some(string_lit.strip_prefix('"')?.strip_suffix('"')?.to_owned()) +pub fn bail(msg: impl AsRef, tokens: T) -> ParseResult +where + T: Spanned, +{ + Err(error(msg, tokens)) } -pub fn bail(msg: impl AsRef, tokens: T) -> Result +pub fn error(msg: impl AsRef, tokens: T) -> Error where T: Spanned, { - Err(Error::new_at_span(tokens.__span(), msg.as_ref())) + Error::new_at_span(tokens.__span(), msg.as_ref()) } pub fn reduce_to_signature(function: &Function) -> Function { @@ -43,25 +44,43 @@ pub fn reduce_to_signature(function: &Function) -> Function { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Key-value parsing of proc attributes -#[derive(Clone, Eq, PartialEq, Debug)] -pub(crate) enum KvValue { - /// Key only, no value. - None, // TODO rename to `Alone`; pending merge conflicts +#[derive(Clone, Debug)] +pub(crate) struct KvValue { + /// Tokens comprising this value. Guaranteed to be nonempty. + tokens: Vec, +} - /// Literal like `"hello"`, `20`, `3.4`. - /// Unlike the proc macro type, this includes `true` and `false` as well as negative literals `-32`. - /// Complex expressions are not supported though. - Lit(String), +impl KvValue { + fn new(tokens: Vec) -> Self { + assert!(!tokens.is_empty()); + Self { tokens } + } + + pub fn expr(self) -> ParseResult { + Ok(self.tokens.into_iter().collect()) + } - /// Identifier like `hello`. - Ident(Ident), + pub fn ident(self) -> ParseResult { + let ident = match &self.tokens[0] { + TokenTree::Ident(ident) => ident.clone(), + tt => { + return bail("expected identifier", tt); + } + }; + if self.tokens.len() > 1 { + return bail( + "expected a single identifier, not an expression", + &self.tokens[1], + ); + } + Ok(ident) + } } -pub(crate) type KvMap = HashMap; +pub(crate) type KvMap = HashMap>; /// Struct to parse attributes like `#[attr(key, key2="value", key3=123)]` in a very user-friendly way. pub(crate) struct KvParser { - attr_name: String, map: KvMap, span: Span, } @@ -100,10 +119,10 @@ impl KvParser { ); } + let attr_name = expected.to_string(); found_attr = Some(Self { - attr_name: expected.to_string(), span: attr.tk_brackets.span, - map: parse_kv_group(&attr.value)?, + map: ParserState::parse(attr_name, &attr.value)?, }); } } @@ -115,60 +134,76 @@ impl KvParser { self.span } - /// `#[attr]`, `#[attr(key)]`, `#[attr(key=Ident)]`, `#[attr(key=Lit)]` - pub fn handle_any(&mut self, key: &str) -> Option { - self.map.remove(key) + /// - For missing keys, returns `None`. + /// - For a key with no value, returns `Some(None)`. + /// - For a key with a value, returns `Some(value)`. + pub fn handle_any(&mut self, key: &str) -> Option> { + self.map.remove(&ident(key)) } - /// `#[attr(key)]` + /// Handles a key that can only occur without a value, e.g. `#[attr(toggle)]`. Returns whether + /// the key is present. pub fn handle_alone(&mut self, key: &str) -> ParseResult { - match self.map.remove(key) { + match self.handle_any(key) { None => Ok(false), - Some(KvValue::None) => Ok(true), - Some(_) => self.bail_key(key, "must not have a value"), + Some(value) => match value { + None => Ok(true), + Some(value) => bail( + format!("key `{key}` should not have a value"), + &value.tokens[0], + ), + }, } } - /// `#[attr(key=Ident)]` + /// Handles an optional key that can only occur with an identifier as the value. pub fn handle_ident(&mut self, key: &str) -> ParseResult> { - match self.map.remove(key) { + match self.map.remove_entry(&ident(key)) { None => Ok(None), - Some(KvValue::Ident(ident)) => Ok(Some(ident)), - Some(_) => self.bail_key(key, "must have an identifier value (no quotes)"), + // The `key` that was removed from the map has the correct span. + Some((key, value)) => match value { + None => bail( + format!("expected `{key}` to be followed by `= identifier`"), + key, + ), + Some(value) => Ok(Some(value.ident()?)), + }, } } - /// `#[attr(key="string", key2=123, key3=true)]` - pub fn handle_lit(&mut self, key: &str) -> ParseResult> { - match self.map.remove(key) { + /// Handles an optional key that can occur with arbitrary tokens as the value. + pub fn handle_expr(&mut self, key: &str) -> ParseResult> { + match self.map.remove_entry(&ident(key)) { None => Ok(None), - Some(KvValue::Lit(string)) => Ok(Some(string)), - Some(_) => self.bail_key(key, "must have a literal value (\"text\", 3.4, true, ...)"), + // The `key` that was removed from the map has the correct span. + Some((key, value)) => match value { + None => bail( + format!("expected `{key}` to be followed by `= expression`"), + key, + ), + Some(value) => Ok(Some(value.expr()?)), + }, } } - /// `#[attr(key="string", key2=123, key3=true)]`, with a given key being required + /// Handles a key that must be provided and must have an identifier as the value. pub fn handle_ident_required(&mut self, key: &str) -> ParseResult { - self.inner_required(key, "ident", Self::handle_ident) - } - - /// `#[attr(key="string", key2=123, key3=true)]`, with a given key being required - pub fn handle_lit_required(&mut self, key: &str) -> ParseResult { - self.inner_required(key, "literal", Self::handle_lit) + self.handle_ident(key)?.ok_or_else(|| { + error( + format!("missing required argument `{key} = identifier`"), + self.span, + ) + }) } - fn inner_required(&mut self, key: &str, context: &str, mut f: F) -> ParseResult - where - F: FnMut(&mut Self, &str) -> ParseResult>, - { - match f(self, key) { - Ok(Some(string)) => Ok(string), - Ok(None) => self.bail_key( - key, - &format!("expected to have {context} value, but is absent"), - ), - Err(err) => Err(err), - } + /// Handles a key that must be provided and must have a value. + pub fn handle_expr_required(&mut self, key: &str) -> ParseResult { + self.handle_expr(key)?.ok_or_else(|| { + error( + format!("missing required argument `{key} = expression`"), + self.span, + ) + }) } /// Explicit "pre-destructor" that must be called, and checks that all map entries have been @@ -180,157 +215,145 @@ impl KvParser { if self.map.is_empty() { Ok(()) } else { - // Useless allocation, but there seems to be no join() on map iterators. Anyway, this is slow/error path. - let keys = self.map.keys().cloned().collect::>().join(", "); - - let s = if self.map.len() > 1 { "s" } else { "" }; // plural - bail( - format!( - "#[{attr}]: unrecognized key{s}: {keys}", - attr = self.attr_name - ), - self.span, - ) + let errors = self + .map + .keys() + .map(|ident| error(format!("unrecognized key `{ident}`"), ident)); + Err(errors + .reduce(|mut a, b| { + a.combine(b); + a + }) + .unwrap()) } } +} - fn bail_key(&self, key: &str, msg: &str) -> ParseResult { - bail( - format!("#[{attr}]: key `{key}` {msg}", attr = self.attr_name), - self.span, - ) - } +struct ParserState<'a> { + attr_name: String, + tokens: std::slice::Iter<'a, TokenTree>, + prev: Option<&'a TokenTree>, + cur: Option<&'a TokenTree>, } -// parses (a="hey", b=342) -pub(crate) fn parse_kv_group(value: &venial::AttributeValue) -> ParseResult { - // FSM with possible flows: - // - // [start]* ------> Key* ----> Equals - // ^ | | - // | v v - // Comma* <----- Value* - // [end] <-- * - #[derive(Copy, Clone, Eq, PartialEq, Debug)] - enum KvState { - Start, - Key, - Equals, - Value, - Comma, - } - - let mut map: KvMap = HashMap::new(); - let mut state = KvState::Start; - let mut last_key: Option = None; - let mut is_negative: bool = false; - - // can't be a closure because closures borrow greedily, and we'd need borrowing only at invocation time (lazy) - macro_rules! insert_kv { - ($value:expr) => { - let key = last_key.take().expect("last_key.take"); - map.insert(key, $value); +impl<'a> ParserState<'a> { + pub fn parse(attr_name: String, attr_value: &'a venial::AttributeValue) -> ParseResult { + let mut tokens = match attr_value { + venial::AttributeValue::Equals(punct, _tokens) => { + return bail("expected `(` or `]`", punct); + } + _ => attr_value.get_value_tokens().iter(), }; - } + let cur = tokens.next(); - let tokens = value.get_value_tokens(); - //println!("all tokens: {tokens:?}"); - for tk in tokens { - // Key - //println!("-- {state:?} -> {tk:?}"); + let parser = Self { + attr_name, + tokens, + prev: None, + cur, + }; - match state { - KvState::Start => match tk { - // key ... - TokenTree::Ident(ident) => { - let key = last_key.replace(ident.to_string()); - assert!(key.is_none()); - state = KvState::Key; - } - _ => bail("attribute must start with key", tk)?, - }, - KvState::Key => { - match tk { - TokenTree::Punct(punct) => { - if punct.as_char() == '=' { - // key = ... - state = KvState::Equals; - } else if punct.as_char() == ',' { - // key, ... - insert_kv!(KvValue::None); - state = KvState::Comma; - } else { - bail("key must be followed by either '=' or ','", tk)?; - } - } - _ => { - bail("key must be followed by either '=' or ','", tk)?; - } - } - } - KvState::Equals => match tk { - // key = value ... - TokenTree::Ident(ident) => { - let ident_str = ident.to_string(); - if ident_str == "true" || ident_str == "false" { - insert_kv!(KvValue::Lit(ident_str)); - } else { - insert_kv!(KvValue::Ident(ident.clone())); + parser.parse_map() + } + + fn parse_map(mut self) -> ParseResult { + let mut map: KvMap = HashMap::new(); + // Whether the previous expression might be missing parentheses. Used only for hints in + // error reporting. + let mut prev_expr_complex = false; + + while let Some(cur) = self.cur { + match cur { + TokenTree::Ident(key) => { + self.next(); + let value = self.parse_opt_value(key, prev_expr_complex)?; + if map.contains_key(key) { + return bail(format!("duplicate key `{key}`"), key); } - state = KvState::Value; - } - // key = "value" ... - TokenTree::Literal(lit) => { - let prefix = if is_negative { "-" } else { "" }; - insert_kv!(KvValue::Lit(format!("{prefix}{lit}"))); - state = KvState::Value; + prev_expr_complex = match &value { + None => false, + Some(value) => value.tokens.len() > 1, + }; + map.insert(key.clone(), value); } - // key = - ... - TokenTree::Punct(punct) if punct.as_char() == '-' => { - is_negative = true; - // state remains - } - _ => bail("'=' sign must be followed by an identifier or literal", tk)?, - }, - KvState::Value => match tk { - // key = value, ... - TokenTree::Punct(punct) => { - if punct.as_char() == ',' { - state = KvState::Comma; + _ => { + let parens_hint = if prev_expr_complex { + let attr = &self.attr_name; + format!("\nnote: the preceding `,` is interpreted as a separator between arguments to `#[{attr}]`; if you meant the `,` as part of an expression, surround the expression with parentheses") } else { - bail("value must be followed by a ','", tk)?; - } + "".to_owned() + }; + return bail(format!("expected identifier{parens_hint}"), cur); } - _ => bail("value must be followed by a ','", tk)?, - }, - KvState::Comma => match tk { - // , key ... - TokenTree::Ident(ident) => { - let key = last_key.replace(ident.to_string()); - assert!(key.is_none()); - is_negative = false; - state = KvState::Key; - } - _ => bail("',' must be followed by the next key", tk)?, - }, + } } - //println!(" {state:?} -> {tk:?}"); + Ok(map) + } + + fn parse_opt_value( + &mut self, + key: &Ident, + prev_expr_complex: bool, + ) -> ParseResult> { + let value = match self.cur { + // End of input directly after a key + None => None, + // Comma following key + Some(tt) if is_punct(tt, ',') => { + self.next(); + None + } + // Equals sign following key + Some(tt) if is_punct(tt, '=') => { + self.next(); + Some(self.parse_value()?) + } + Some(tt) => { + let parens_hint = if prev_expr_complex { + let attr = &self.attr_name; + format!("\nnote: `{key}` is interpreted as the next argument to `#[{attr}]`; if you meant it as part of an expression, surround the expression with parentheses") + } else { + "".to_owned() + }; + return bail( + format!("expected next argument, or `= value` following `{key}`{parens_hint}"), + tt, + ); + } + }; + Ok(value) } - // No more tokens, make sure it ends in a valid state - match state { - KvState::Key => { - // Only stored key, not yet added to map - insert_kv!(KvValue::None); + fn parse_value(&mut self) -> ParseResult { + let mut tokens = Vec::new(); + while let Some(cur) = self.cur { + if is_punct(cur, ',') { + self.next(); + break; + } + tokens.push(cur.clone()); + self.next(); } - KvState::Start | KvState::Value | KvState::Comma => {} - KvState::Equals => { - bail("unexpected end of macro attributes", value)?; + if tokens.is_empty() { + // `cur` might be `None` at this point, so we point at the previous token instead. + // This could be the `=` sign or a `,` directly after `=`. + return bail("expected value after `=`", self.prev.unwrap()); } + Ok(KvValue::new(tokens)) + } + + fn next(&mut self) { + self.prev = self.cur; + self.cur = self.tokens.next(); } +} - Ok(map) +fn is_punct(tt: &TokenTree, c: char) -> bool { + match tt { + TokenTree::Punct(punct) => punct.as_char() == c, + _ => false, + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -431,22 +454,42 @@ mod tests { use proc_macro2::TokenStream; use quote::quote; - macro_rules! hash_map { + /// A quick and dirty way to compare two expressions for equality. Only for unit tests; not + /// very suitable for production code. + impl PartialEq for KvValue { + fn eq(&self, other: &Self) -> bool { + let to_strings = |kv: &Self| { + kv.tokens + .iter() + .map(ToString::to_string) + .collect::>() + }; + to_strings(self) == to_strings(other) + } + } + + macro_rules! kv_map { ( - $($key:expr => $value:expr),* + $($key:ident => $value:expr),* $(,)? ) => { { let mut map = std::collections::HashMap::new(); $( - map.insert($key, $value); + map.insert(ident(stringify!($key)), $value); )* map } }; } - fn expect_parsed(input_tokens: TokenStream, output_map: KvMap) { + macro_rules! kv_value { + ($($args:tt)*) => { + KvValue::new(quote!($($args)*).into_iter().collect()) + } + } + + fn parse(input_tokens: TokenStream) -> KvMap { let input = quote! { #input_tokens fn func(); @@ -462,10 +505,18 @@ mod tests { assert_eq!(attrs.len(), 1); let attr_value = &attrs[0].value; - let mut parsed = parse_kv_group(attr_value).expect("parse"); + ParserState::parse("attr".to_owned(), attr_value).expect("parse") + } + + fn expect_parsed(input_tokens: TokenStream, output_map: KvMap) { + let mut parsed = parse(input_tokens); for (key, value) in output_map { - assert_eq!(parsed.remove(&key), Some(value)); + assert_eq!( + parsed.remove(&key), + Some(value), + "incorrect parsed value for `{key}`" + ); } assert!(parsed.is_empty(), "Remaining entries in map"); @@ -477,52 +528,88 @@ mod tests { quote! { #[attr(just_key)] }, - hash_map!( - "just_key".to_string() => KvValue::None, + kv_map!( + just_key => None, + ), + ); + } + + #[test] + fn test_parse_kv_ident() { + expect_parsed( + quote! { + #[attr(key = value)] + }, + kv_map!( + key => Some(kv_value!(value)), + ), + ); + } + + #[test] + fn test_parse_kv_trailing_comma() { + expect_parsed( + quote! { + #[attr(key = value,)] + }, + kv_map!( + key => Some(kv_value!(value)), ), ); } #[test] - fn test_parse_kv_key_ident() { + fn test_parse_kv_first_last_expr() { expect_parsed( quote! { - #[attr(key=value)] + #[attr(first = foo, middle = bar, last = qux)] }, - hash_map!( - "key".to_string() => KvValue::Ident(ident("value")), + kv_map!( + first => Some(kv_value!(foo)), + middle => Some(kv_value!(bar)), + last => Some(kv_value!(qux)), ), ); } #[test] - fn test_parse_kv_key_lit() { + fn test_parse_kv_first_last_alone() { expect_parsed( quote! { - #[attr(key="string", pos=32, neg=-32, bool=true, float=3.4)] + #[attr(first, middle = bar, last)] }, - hash_map!( - "key".to_string() => KvValue::Lit("\"string\"".to_string()), - "pos".to_string() => KvValue::Lit("32".to_string()), - "neg".to_string() => KvValue::Lit("-32".to_string()), - "bool".to_string() => KvValue::Lit("true".to_string()), - "float".to_string() => KvValue::Lit("3.4".to_string()), + kv_map!( + first => None, + middle => Some(kv_value!(bar)), + last => None, ), ); } #[test] - fn test_parse_kv_mixed() { + fn test_parse_kv_exprs() { expect_parsed( quote! { - #[attr(forever, key="string", default=-820, fn=my_function, alone)] + #[attr( + pos = 42, + neg = -42, + str_lit = "string", + sum = 1 + 1, + vec = Vector2::new(1.0, -1.0e2), + // Currently needs parentheses. + generic = (HashMap::>::new()), + // Currently needs parentheses. + closure = (|a: &u32, b: &u32| a + b), + )] }, - hash_map!( - "forever".to_string() => KvValue::None, - "key".to_string() => KvValue::Lit("\"string\"".to_string()), - "default".to_string() => KvValue::Lit("-820".to_string()), - "fn".to_string() => KvValue::Ident(ident("my_function")), - "alone".to_string() => KvValue::None, + kv_map!( + pos => Some(kv_value!(42)), + neg => Some(kv_value!(-42)), + str_lit => Some(kv_value!("string")), + sum => Some(kv_value!(1 + 1)), + vec => Some(kv_value!(Vector2::new(1.0, -1.0e2))), + generic => Some(kv_value!((HashMap::>::new()))), + closure => Some(kv_value!((|a: &u32, b: &u32| a + b))), ), ); } diff --git a/itest/rust/src/export_test.rs b/itest/rust/src/export_test.rs index 1e5d1d06d..bc85a0810 100644 --- a/itest/rust/src/export_test.rs +++ b/itest/rust/src/export_test.rs @@ -16,24 +16,24 @@ struct HasProperty { #[export] int_val: i32, - #[export(get = "get_int_val_read")] + #[export(get = get_int_val_read)] int_val_read: i32, - #[export(set = "set_int_val_write")] + #[export(set = set_int_val_write)] int_val_write: i32, - #[export(get = "get_int_val_rw", set = "set_int_val_rw")] + #[export(get = get_int_val_rw, set = set_int_val_rw)] int_val_rw: i32, - #[export(get = "get_int_val_getter", set)] + #[export(get = get_int_val_getter, set)] int_val_getter: i32, - #[export(get, set = "set_int_val_setter")] + #[export(get, set = set_int_val_setter)] int_val_setter: i32, - #[export(get = "get_string_val", set = "set_string_val")] + #[export(get = get_string_val, set = set_string_val)] string_val: GodotString, - #[export(get = "get_object_val", set = "set_object_val")] + #[export(get = get_object_val, set = set_object_val)] object_val: Option>, #[export] texture_val: Gd, - #[export(get = "get_texture_val", set = "set_texture_val", hint = PROPERTY_HINT_RESOURCE_TYPE, hint_desc = "Texture")] + #[export(get = get_texture_val, set = set_texture_val, hint = PROPERTY_HINT_RESOURCE_TYPE, hint_desc = "Texture")] texture_val_rw: Option>, }