use proc_macro2::{Ident, Span, TokenStream};
use quote::{ToTokens, format_ident, quote, quote_spanned};
use venial::TypeExpr;
use crate::class::{
Field, FieldHint, FuncDefinition, into_signature_info, make_accessor_type_check,
make_method_registration,
};
use crate::util::{KvParser, ident, make_funcs_collection_constant};
use crate::{ParseResult, util};
#[derive(Clone, Debug)]
pub struct FieldVar {
pub rename: Option<String>,
pub getter: GetterSetter,
pub setter: GetterSetter,
pub hint: FieldHint,
pub usage_flags: UsageFlags,
pub rust_public: bool,
pub span: Span,
}
impl FieldVar {
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
let span = parser.span();
let rename = parser.handle_ident("rename")?.map(|i| i.to_string());
let getter = GetterSetter::parse(parser, "get")?;
let setter = GetterSetter::parse(parser, "set")?;
let rust_public = parser.handle_alone("pub")?;
if rust_public && getter != GetterSetter::Generated && setter != GetterSetter::Generated {
return Err(util::error!(
span,
"`pub` requires at least one generated accessor; both getter and setter are user-defined or disabled"
));
}
let hint = parser.handle_ident("hint")?;
let hint = if let Some(hint) = hint {
let hint_string = parser.handle_expr("hint_string")?;
FieldHint::new(hint, hint_string)
} else {
FieldHint::Inferred
};
let usage_flags = match parser.handle_array("usage_flags")? {
Some(mut parser) => {
let mut flags = Vec::new();
while let Some(flag) = parser.next_ident()? {
flags.push(flag)
}
parser.finish()?;
UsageFlags::Custom(flags)
}
_ => UsageFlags::Inferred,
};
Ok(FieldVar {
rename,
getter,
setter,
hint,
usage_flags,
rust_public,
span,
})
}
pub(crate) fn new_tool_button_from_kv(
parser: &mut KvParser,
field_name: &Ident,
) -> ParseResult<Self> {
let span = parser.span();
let hint_string = {
let name = if let Some(lit) = parser.handle_literal("name", "String")? {
lit.to_string()
} else {
field_name.to_string().replace("_", " ")
};
if let Some(icon) = parser.handle_literal("icon", "String")? {
let unquoted_name = name.trim_matches('\"');
let unquoted_icon = icon.to_string();
let unquoted_icon = unquoted_icon.trim_matches('\"');
format!("{unquoted_name},{unquoted_icon}")
} else {
name
}
};
let Some(tool_button_fn) = parser.handle_expr("fn")? else {
return Err(util::error!(
span,
"`#[export_tool_button]` requires `fn` attribute.\n \
Tip: use `#[export_tool_button(fn = ...)]`."
));
};
let hint = FieldHint::new(ident("TOOL_BUTTON"), Some(hint_string.to_token_stream()));
Ok(FieldVar {
rename: None,
getter: GetterSetter::ToolButton(ToolButtonFn(tool_button_fn)),
setter: GetterSetter::Disabled,
hint,
usage_flags: UsageFlags::Custom(vec![ident("EDITOR")]),
rust_public: true,
span,
})
}
}
impl Default for FieldVar {
fn default() -> Self {
Self {
rename: Default::default(),
getter: Default::default(),
setter: Default::default(),
hint: Default::default(),
usage_flags: Default::default(),
rust_public: false,
span: Span::call_site(),
}
}
}
#[derive(Default, Clone, Debug)]
pub(crate) struct ToolButtonFn(TokenStream);
impl Eq for ToolButtonFn {}
impl PartialEq for ToolButtonFn {
fn eq(&self, other: &Self) -> bool {
self.0.to_string() == other.0.to_string()
}
}
#[derive(Default, Clone, Eq, PartialEq, Debug)]
pub enum GetterSetter {
#[default]
Generated,
Custom,
CustomRenamed(Ident),
ToolButton(ToolButtonFn),
Disabled,
}
impl GetterSetter {
pub(super) fn parse(parser: &mut KvParser, key: &str) -> ParseResult<Self> {
if parser.handle_alone(&format!("no_{key}"))? {
return Ok(GetterSetter::Disabled);
}
let getter_setter = match parser.handle_any(key) {
None => GetterSetter::Generated,
Some(value) => match value {
None => GetterSetter::Custom,
Some(value) => GetterSetter::CustomRenamed(value.ident()?),
},
};
Ok(getter_setter)
}
pub(super) fn to_impl(
&self,
class_name: &Ident,
kind: GetSet,
field: &Field,
rename: Option<&str>,
rust_public: bool,
) -> Option<GetterSetterImpl> {
match self {
GetterSetter::Disabled => match kind {
GetSet::Get => Some(GetterSetterImpl::from_write_only_getter(
class_name, field, rename,
)),
GetSet::Set => None,
},
GetterSetter::Generated => Some(GetterSetterImpl::from_generated_impl(
class_name,
kind,
field,
rename,
rust_public,
None,
)),
GetterSetter::Custom => Some(GetterSetterImpl::from_custom_impl(
class_name, kind, field, rename,
)),
GetterSetter::CustomRenamed(function_name) => Some(
GetterSetterImpl::from_custom_impl_renamed(class_name, kind, field, function_name),
),
GetterSetter::ToolButton(tool_button_fn) => {
Some(GetterSetterImpl::from_generated_impl(
class_name,
kind,
field,
rename,
rust_public,
Some(&tool_button_fn.0),
))
}
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum GetSet {
Get,
Set,
}
impl GetSet {
pub fn make_pub_fn_name(self, field_name: &Ident, rename: Option<&str>) -> Ident {
let prefix = self.prefix();
if let Some(rename) = rename {
format_ident!("{prefix}{rename}", span = field_name.span())
} else {
format_ident!("{prefix}{field_name}", span = field_name.span())
}
}
fn prefix(self) -> &'static str {
match self {
GetSet::Get => "get_",
GetSet::Set => "set_",
}
}
}
#[derive(Clone, Debug)]
pub struct GetterSetterImpl {
pub rust_accessor: Ident,
pub function_impl: TokenStream,
pub export_token: TokenStream,
pub funcs_collection_constant: TokenStream,
}
impl GetterSetterImpl {
fn from_generated_impl(
class_name: &Ident,
kind: GetSet,
field: &Field,
rename: Option<&str>,
rust_public: bool,
tool_button_fn: Option<&TokenStream>,
) -> Self {
let Field {
name: field_name,
ty: field_type,
..
} = field;
let field_ty_span = field_type.span();
let field_span = field_name.span();
let godot_function_name = kind.make_pub_fn_name(field_name, rename);
let rust_accessor;
let doc_hidden;
let deprecated_function;
let signature;
let function_body;
if let Some(tool_button_fn) = tool_button_fn {
rust_accessor = godot_function_name.clone();
doc_hidden = quote! { #[doc(hidden)] };
deprecated_function = TokenStream::new();
(signature, function_body) = GetterSetterImpl::generate_tool_button_impl(
field_name,
field_type,
&rust_accessor,
tool_button_fn,
);
} else if rust_public {
rust_accessor = godot_function_name.clone();
doc_hidden = TokenStream::new();
deprecated_function = TokenStream::new();
match kind {
GetSet::Get => {
signature = quote_spanned! { field_ty_span=>
fn #rust_accessor(&self) -> <#field_type as ::godot::register::property::Var>::PubType
};
function_body = quote_spanned! { field_ty_span=>
<#field_type as ::godot::register::property::Var>::var_pub_get(&self.#field_name)
};
}
GetSet::Set => {
signature = quote_spanned! { field_ty_span=>
fn #rust_accessor(&mut self, #field_name: <#field_type as ::godot::register::property::Var>::PubType)
};
function_body = quote_spanned! { field_ty_span=>
<#field_type as ::godot::register::property::Var>::var_pub_set(&mut self.#field_name, #field_name)
};
}
}
} else {
let prefix = kind.prefix();
rust_accessor = format_ident!("__godot_{prefix}{field_name}", span = field_span);
doc_hidden = quote! { #[doc(hidden)] };
let deprecated_fn = match kind {
GetSet::Get => quote_spanned! { field_ty_span=>
pub fn #godot_function_name(&self) -> <#field_type as ::godot::meta::GodotConvert>::Via {
self.#rust_accessor()
}
},
GetSet::Set => quote_spanned! { field_ty_span=>
pub fn #godot_function_name(&mut self, #field_name: <#field_type as ::godot::meta::GodotConvert>::Via) {
self.#rust_accessor(#field_name)
}
},
};
deprecated_function = quote! {
#[deprecated = "Auto-generated Rust getters/setters for `#[var]` are being phased out until v0.6.\n\
If you need them, opt in with #[var(pub)]."]
#[allow(dead_code)] #deprecated_fn
};
match kind {
GetSet::Get => {
signature = quote_spanned! { field_ty_span=>
fn #rust_accessor(&self) -> <#field_type as ::godot::meta::GodotConvert>::Via
};
function_body = quote_spanned! { field_ty_span=>
<#field_type as ::godot::register::property::Var>::var_get(&self.#field_name)
};
}
GetSet::Set => {
signature = quote_spanned! { field_ty_span=>
fn #rust_accessor(&mut self, #field_name: <#field_type as ::godot::meta::GodotConvert>::Via)
};
function_body = quote_spanned! { field_ty_span=>
<#field_type as ::godot::register::property::Var>::var_set(&mut self.#field_name, #field_name)
};
}
}
}
let function_impl = quote_spanned! { field_span=>
#doc_hidden
pub #signature {
#function_body
}
#deprecated_function
};
let (funcs_collection_constant, export_token) =
Self::make_registration(class_name, &rust_accessor, &godot_function_name, signature);
Self {
rust_accessor,
function_impl,
export_token,
funcs_collection_constant,
}
}
fn from_write_only_getter(class_name: &Ident, field: &Field, rename: Option<&str>) -> Self {
let Field {
name: field_name,
ty: field_type,
..
} = field;
let field_span = field_name.span();
let prefix = GetSet::Get.prefix();
let godot_function_name =
format_ident!("__disabled_{prefix}{field_name}", span = field_span);
let rust_accessor = format_ident!("__godot_{prefix}{field_name}", span = field_span);
let method_name = match rename {
Some(rename) => rename.to_string(),
None => field_name.to_string(),
};
let panic_message =
format!("property '{class_name}::{method_name}' is write-only through #[var(no_get)]");
let signature = quote_spanned! { field_span=>
fn #rust_accessor(&self) -> <#field_type as ::godot::meta::GodotConvert>::Via
};
let function_impl = quote_spanned! { field_span=>
#[doc(hidden)]
pub #signature {
panic!(#panic_message)
}
};
let (funcs_collection_constant, export_token) =
Self::make_registration(class_name, &rust_accessor, &godot_function_name, signature);
Self {
rust_accessor,
function_impl,
export_token,
funcs_collection_constant,
}
}
fn make_registration(
class_name: &Ident,
rust_accessor: &Ident,
godot_function_name: &Ident,
signature: TokenStream,
) -> (TokenStream, TokenStream) {
let funcs_collection_constant = make_funcs_collection_constant(
class_name,
rust_accessor,
Some(&godot_function_name.to_string()),
&[],
);
let signature = util::parse_signature(signature);
let export_token = make_method_registration(
class_name,
FuncDefinition {
signature_info: into_signature_info(signature, class_name, false),
external_attributes: Vec::new(),
registered_name: Some(godot_function_name.to_string()),
is_script_virtual: false,
rpc_info: None,
is_generated_accessor: true,
},
None,
);
let export_token = export_token.expect("accessor registration should not fail");
(funcs_collection_constant, export_token)
}
fn generate_tool_button_impl(
field_name: &Ident,
field_type: &TypeExpr,
rust_accessor: &Ident,
tool_button_fn: &TokenStream,
) -> (TokenStream, TokenStream) {
let field_ty_span = field_type.span();
let callable_name = format!("tool_button_{field_name}");
let signature = quote_spanned! { field_ty_span=>
fn #rust_accessor(&self) -> <#field_type as ::godot::register::property::Var>::PubType
};
let function_body = quote_spanned! { field_ty_span=>
let mut obj = self.to_gd();
#[allow(clippy::redundant_closure_call, clippy::explicit_auto_deref)]
Callable::from_fn(#callable_name, move |_args| (#tool_button_fn)(&mut *obj.bind_mut()))
};
(signature, function_body)
}
fn from_custom_impl_renamed(
class_name: &Ident,
kind: GetSet,
field: &Field,
function_name: &Ident,
) -> Self {
let export_token = make_accessor_type_check(class_name, function_name, &field.ty, kind);
Self {
rust_accessor: function_name.clone(),
function_impl: TokenStream::new(),
export_token,
funcs_collection_constant: TokenStream::new(),
}
}
fn from_custom_impl(
class_name: &Ident,
kind: GetSet,
field: &Field,
rename: Option<&str>,
) -> Self {
let function_name = kind.make_pub_fn_name(&field.name, rename);
let export_token = make_accessor_type_check(class_name, &function_name, &field.ty, kind);
Self {
rust_accessor: function_name,
function_impl: TokenStream::new(),
export_token,
funcs_collection_constant: TokenStream::new(),
}
}
}
#[derive(Default, Clone, Debug)]
pub enum UsageFlags {
#[default]
Inferred,
InferredExport,
Custom(Vec<Ident>),
}
impl UsageFlags {
pub fn is_inferred(&self) -> bool {
matches!(self, Self::Inferred)
}
}