use proc_macro2::{Ident, Punct, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use venial::Error;
use crate::class::data_models::fields::{Fields, named_fields};
use crate::class::data_models::group_export::FieldGroup;
use crate::class::{
BeforeKind, Field, FieldCond, FieldDefault, FieldExport, FieldVar, GetterSetter, SignatureInfo,
make_property_impl, make_virtual_callback,
};
use crate::util::{
KvParser, bail, error, format_funcs_collection_struct, ident, ident_respan,
path_ends_with_complex, require_api_version,
};
use crate::{ParseResult, handle_mutually_exclusive_keys, util};
pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
let class = item.as_struct().ok_or_else(|| {
util::error_fn(
"#[derive(GodotClass)] is only allowed on structs",
item.name(),
)
})?;
if class.generic_params.is_some() {
return bail!(
&class.generic_params,
"#[derive(GodotClass)] does not support lifetimes or generic parameters",
);
}
let mut modifiers = Vec::new();
let named_fields = named_fields(class, "#[derive(GodotClass)]")?;
let mut struct_cfg = parse_struct_attributes(class)?;
let mut fields = parse_fields(named_fields, struct_cfg.init_strategy)?;
if fields.has_tool_button {
if !struct_cfg.is_tool {
return bail!(
&class.name,
"`#[export_tool_button]` requires `#[class(tool)]`.",
);
}
if fields.base_field.is_none() {
return bail!(
&class.name,
"`#[export_tool_button]` requires the `Base<T>` field.",
);
}
}
if struct_cfg.is_editor_plugin() {
modifiers.push(quote! { with_editor_plugin })
}
let mut deprecations = std::mem::take(&mut struct_cfg.deprecations);
deprecations.append(&mut fields.deprecations);
let errors = fields.errors.iter().map(|error| error.to_compile_error());
let class_name = &class.name;
let class_name_str: String = struct_cfg
.rename
.unwrap_or_else(|| class.name.clone())
.to_string();
let class_name_allocation = quote! { ClassId::__alloc_next_unicode(#class_name_str) };
if struct_cfg.is_internal {
modifiers.push(quote! { with_internal })
}
let base_ty = &struct_cfg.base_ty;
let prv = quote! { ::godot::private };
let struct_docs_registration = crate::docs::make_struct_docs_registration(
base_ty.to_string(),
&class.attributes,
&fields.all_fields,
class_name,
&prv,
);
let base_class = quote! { ::godot::classes::#base_ty };
let inherits_macro_ident = format_ident!(
"inherit_from_{base_ty}__ensure_class_exists",
span = base_ty.span()
);
let godot_exports_impl = make_property_impl(class_name, &fields);
let godot_withbase_impl = make_with_base_impl(&fields.base_field, class_name);
let (user_singleton_impl, singleton_init_level_const) = if struct_cfg.is_singleton {
modifiers.push(quote! { with_singleton::<#class_name> });
make_singleton_impl(class_name)
} else {
(TokenStream::new(), TokenStream::new())
};
let (user_class_impl, has_default_virtual) = make_user_class_impl(
class_name,
&struct_cfg.base_ty,
struct_cfg.is_tool,
&fields.all_fields,
);
let mut init_expecter = TokenStream::new();
let mut godot_init_impl = TokenStream::new();
let mut is_instantiable = true;
match struct_cfg.init_strategy {
InitStrategy::Generated => {
godot_init_impl = make_godot_init_impl(class_name, &fields);
modifiers.push(quote! { with_generated::<#class_name> });
}
InitStrategy::UserDefined => {
let fn_name = format_ident!(
"class_{class_name}_must_have_an_init_method",
span = class_name.span()
);
init_expecter = quote! {
#[allow(non_snake_case)]
fn #fn_name() {
fn __type_check<T: ::godot::obj::cap::GodotDefault>() {}
__type_check::<#class_name>();
}
}
}
InitStrategy::Absent => {
is_instantiable = false;
#[cfg(before_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.5")))]
modifiers.push(quote! { with_generated_no_default::<#class_name> });
}
};
if is_instantiable {
modifiers.push(quote! { with_instantiable });
}
if has_default_virtual {
modifiers.push(quote! { with_default_get_virtual_fn::<#class_name> });
}
if struct_cfg.is_tool {
modifiers.push(quote! { with_tool })
}
let funcs_collection_struct_name = format_funcs_collection_struct(class_name);
let class_vis = class.vis_marker.as_ref();
let funcs_collection_struct = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
#class_vis struct #funcs_collection_struct_name {}
};
let visibility_macro = make_visibility_macro(class_name, class.vis_marker.as_ref());
let base_field_macro = make_base_field_macro(class_name, fields.base_field.is_some());
let deny_manual_init_macro = make_deny_manual_init_macro(class_name, struct_cfg.init_strategy);
Ok(quote! {
impl ::godot::obj::GodotClass for #class_name {
type Base = #base_class;
#singleton_init_level_const
fn class_id() -> ::godot::meta::ClassId {
use ::godot::meta::ClassId;
static CLASS_ID: std::sync::OnceLock<ClassId> = std::sync::OnceLock::new();
let id: &'static ClassId = CLASS_ID.get_or_init(|| #class_name_allocation);
*id
}
}
unsafe impl ::godot::obj::Bounds for #class_name {
type Memory = <<Self as ::godot::obj::GodotClass>::Base as ::godot::obj::Bounds>::Memory;
type DynMemory = <<Self as ::godot::obj::GodotClass>::Base as ::godot::obj::Bounds>::DynMemory;
type Declarer = ::godot::obj::bounds::DeclUser;
type Exportable = <<Self as ::godot::obj::GodotClass>::Base as ::godot::obj::Bounds>::Exportable;
}
#funcs_collection_struct
#godot_init_impl
#godot_withbase_impl
#godot_exports_impl
#user_class_impl
#init_expecter
#visibility_macro
#base_field_macro
#deny_manual_init_macro
#( #deprecations )*
#( #errors )*
#user_singleton_impl
#struct_docs_registration
::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>(
#prv::PluginItem::Struct(
#prv::Struct::new::<#class_name>()#(.#modifiers())*
)
));
#prv::class_macros::#inherits_macro_ident!(#class_name);
})
}
fn make_with_base_impl(base_field: &Option<Field>, class_name: &Ident) -> TokenStream {
let Some(Field { name, ty, .. }) = base_field else {
return TokenStream::new();
};
quote_spanned! { ty.span()=>
impl ::godot::obj::WithBaseField for #class_name {
fn to_gd(&self) -> ::godot::obj::Gd<#class_name> {
let base = <#class_name as ::godot::obj::WithBaseField>::base_field(self);
base.__constructed_gd().cast()
}
fn base_field(&self) -> &::godot::obj::Base<<#class_name as ::godot::obj::GodotClass>::Base> {
&self.#name
}
}
}
}
fn make_singleton_impl(class_name: &Ident) -> (TokenStream, TokenStream) {
(
quote! {
impl ::godot::obj::UserSingleton for #class_name {}
},
quote! {
const INIT_LEVEL: ::godot::init::InitLevel = ::godot::init::InitLevel::Scene;
},
)
}
fn make_visibility_macro(
class_name: &Ident,
vis_marker: Option<&venial::VisMarker>,
) -> TokenStream {
let macro_name = util::format_class_visibility_macro(class_name);
quote! {
macro_rules! #macro_name {
(
$( #[$meta:meta] )*
struct $( $tt:tt )+
) => {
$( #[$meta] )*
#vis_marker struct $( $tt )+
};
}
}
}
fn make_base_field_macro(class_name: &Ident, has_base_field: bool) -> TokenStream {
let macro_name = util::format_class_base_field_macro(class_name);
if has_base_field {
quote! {
macro_rules! #macro_name {
( $( $tt:tt )* ) => { $( $tt )* };
}
}
} else {
quote! {
macro_rules! #macro_name {
( $( $tt:tt )* ) => {};
}
}
}
}
fn make_deny_manual_init_macro(class_name: &Ident, init_strategy: InitStrategy) -> TokenStream {
let macro_name = util::format_class_deny_manual_init_macro(class_name);
let class_attr = match init_strategy {
InitStrategy::Absent => "#[class(no_init)]",
InitStrategy::Generated => "#[class(init)]",
InitStrategy::UserDefined => {
return quote! {
macro_rules! #macro_name {
() => {};
}
};
}
};
let error_message =
format!("Class `{class_name}` is marked with {class_attr} but provides an init() method.");
quote! {
macro_rules! #macro_name {
() => {
compile_error!(#error_message);
};
}
}
}
pub fn make_accessor_type_check(
class_name: &Ident,
accessor_name: &Ident,
field_type: &venial::TypeExpr,
kind: crate::class::GetSet,
) -> TokenStream {
use crate::class::GetSet;
let accessor_span = accessor_name.span();
let class_name = ident_respan(class_name, accessor_span);
match kind {
GetSet::Get => quote_spanned! { accessor_span=>
::godot::private::typecheck_getter::<#class_name, #field_type>(
#class_name::#accessor_name
)
},
GetSet::Set => quote_spanned! { accessor_span=>
::godot::private::typecheck_setter::<#class_name, #field_type>(
#[allow(clippy::redundant_closure)] |this, val| #class_name::#accessor_name(this, val)
)
},
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum InitStrategy {
Generated,
UserDefined,
Absent,
}
struct ClassAttributes {
base_ty: Ident,
init_strategy: InitStrategy,
is_tool: bool,
is_singleton: bool,
is_internal: bool,
rename: Option<Ident>,
deprecations: Vec<TokenStream>,
}
impl ClassAttributes {
fn is_editor_plugin(&self) -> bool {
self.base_ty == ident("EditorPlugin")
}
}
fn make_godot_init_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
let base_init = if let Some(Field { name, ty, .. }) = &fields.base_field {
quote_spanned! { ty.span()=> #name: base, }
} else {
TokenStream::new()
};
let rest_init = fields.all_fields.iter().map(|field| {
let field_name = field.name.clone();
let value_expr = field
.default_val
.clone()
.map(|field| field.default_val)
.unwrap_or_else(|| quote_spanned! { field.span=> ::std::default::Default::default() });
quote! { #field_name: #value_expr, }
});
quote! {
impl ::godot::obj::cap::GodotDefault for #class_name {
fn __godot_user_init(base: ::godot::obj::Base<<#class_name as ::godot::obj::GodotClass>::Base>) -> Self {
Self {
#( #rest_init )*
#base_init
}
}
}
}
}
fn make_onready_init(all_fields: &[Field]) -> TokenStream {
let onready_fields = all_fields
.iter()
.filter(|&field| field.is_onready)
.map(|field| {
let field = &field.name;
quote! {
::godot::private::auto_init(&mut self.#field, &base);
}
})
.collect::<Vec<_>>();
if !onready_fields.is_empty() {
quote! {
{
let base = <Self as ::godot::obj::WithBaseField>::to_gd(self).upcast();
#( #onready_fields )*
}
}
} else {
TokenStream::new()
}
}
fn make_oneditor_panic_inits(class_name: &Ident, all_fields: &[Field]) -> TokenStream {
let is_in_editor = quote! { <::godot::classes::Engine as ::godot::obj::Singleton>::singleton().is_editor_hint() };
let on_editor_fields_checks = all_fields
.iter()
.filter(|&field| field.is_oneditor)
.map(|field| {
let field = &field.name;
let field_name_str = field.to_string();
quote! {
if this.#field.is_invalid() {
uninitialized_fields.push(#field_name_str);
}
}
})
.collect::<Vec<_>>();
if !on_editor_fields_checks.is_empty() {
let class_name_str = class_name.to_string();
quote! {
fn __check_oneditor_fields(this: &#class_name) {
if #is_in_editor {
return;
}
let mut uninitialized_fields: Vec<&str> = Vec::new();
#( #on_editor_fields_checks )*
if !uninitialized_fields.is_empty() {
panic!(
"{}::ready(): OnEditor fields not initialized: {}",
#class_name_str,
uninitialized_fields.join(", "),
);
}
}
__check_oneditor_fields(&self);
}
} else {
TokenStream::new()
}
}
fn make_user_class_impl(
class_name: &Ident,
trait_base_class: &Ident,
is_tool: bool,
all_fields: &[Field],
) -> (TokenStream, bool) {
#[cfg(feature = "codegen-full")] #[cfg_attr(published_docs, doc(cfg(feature = "codegen-full")))]
let rpc_registrations =
quote! { ::godot::register::private::auto_register_rpcs::<#class_name>(self); };
#[cfg(not(feature = "codegen-full"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "codegen-full"))))]
let rpc_registrations = TokenStream::new();
let onready_inits = make_onready_init(all_fields);
let oneditor_panic_inits = make_oneditor_panic_inits(class_name, all_fields);
let run_before_ready = !onready_inits.is_empty() || !oneditor_panic_inits.is_empty();
let default_virtual_fn = if run_before_ready {
let tool_check = util::make_virtual_tool_check();
let signature_info = SignatureInfo::fn_ready();
let callback = make_virtual_callback(
class_name,
trait_base_class,
&signature_info,
BeforeKind::OnlyBefore,
None,
);
let (hash_param, matches_ready_hash);
if cfg!(since_api = "4.4") {
hash_param = quote! { hash: u32, };
matches_ready_hash = quote! {
(name, hash) == ::godot::private::virtuals::Node::ready
};
} else {
hash_param = TokenStream::new();
matches_ready_hash = quote! { name == "_ready" }
}
let default_virtual_fn = quote! {
fn __default_virtual_call(
name: &str,
#hash_param
) -> ::godot::sys::GDExtensionClassCallVirtual {
use ::godot::obj::UserClass as _;
#tool_check
if #matches_ready_hash {
#callback
} else {
None
}
}
};
Some(default_virtual_fn)
} else {
None
};
let user_class_impl = quote! {
impl ::godot::obj::UserClass for #class_name {
#[doc(hidden)]
fn __config() -> ::godot::private::ClassConfig {
::godot::private::ClassConfig {
is_tool: #is_tool,
}
}
#[doc(hidden)]
fn __before_ready(&mut self) {
#oneditor_panic_inits
#rpc_registrations
#onready_inits
}
#default_virtual_fn
}
};
(user_class_impl, default_virtual_fn.is_some())
}
fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttributes> {
let mut base_ty = None;
let mut init_strategy = InitStrategy::UserDefined;
let mut is_tool = false;
let mut is_singleton = false;
let mut is_internal = false;
let mut rename: Option<Ident> = None;
#[allow(unused_mut)] let mut deprecations = vec![];
if let Some(mut parser) = KvParser::parse(&class.attributes, "class")? {
if let Some(base) = parser.handle_ident("base")? {
base_ty = Some(base);
}
match handle_opposite_keys(&mut parser, "init", "class")? {
Some(true) => init_strategy = InitStrategy::Generated,
Some(false) => init_strategy = InitStrategy::Absent,
None => {}
}
if parser.handle_alone("tool")? {
is_tool = true;
}
if parser.handle_alone("singleton")? {
is_singleton = true;
is_tool = true;
}
if let Some(key) = parser.handle_alone_with_span("editor_plugin")? {
return bail!(
key,
"#[class(editor_plugin)] has been removed in favor of #[class(tool, base=EditorPlugin)]",
);
}
rename = parser.handle_ident("rename")?;
if parser.handle_alone("internal")? {
is_internal = true;
} else {
if class.name.to_string().starts_with("Editor") {
return bail!(
class.name.span(),
"Classes starting with `Editor` are implicitly hidden by Godot; use #[class(internal)] to make this explicit",
);
}
}
if let Some(key) = parser.handle_alone_with_span("hidden")? {
return bail!(
key,
"#[class(hidden)] has been renamed to #[class(internal)]",
);
}
parser.finish()?;
}
let base_ty = base_field_or_default(base_ty, is_singleton);
if init_strategy == InitStrategy::Absent && base_ty == ident("EditorPlugin") {
return bail!(
class,
"\n#[class(no_init, base=EditorPlugin)] will crash when opened in the editor.\n\
EditorPlugin classes are automatically instantiated by Godot and require a default constructor.\n\
Use #[class(init)] instead, or provide a custom init() function in the IEditorPlugin impl."
);
}
if init_strategy == InitStrategy::Absent && is_singleton {
return bail!(
class,
"#[class(singleton)] can't be used with #[class(no_init)]",
);
}
post_validate(&base_ty, is_tool)?;
Ok(ClassAttributes {
base_ty,
init_strategy,
is_tool,
is_singleton,
is_internal,
rename,
deprecations,
})
}
fn parse_fields(
named_fields: Vec<(venial::NamedField, Punct)>,
init_strategy: InitStrategy,
) -> ParseResult<Fields> {
let mut all_fields = vec![];
let mut base_field = Option::<Field>::None;
#[allow(unused_mut)] let mut deprecations = vec![];
let mut errors = vec![];
let mut has_tool_button = false;
for (named_field, _punct) in named_fields {
let mut is_base = false;
let mut field = Field::new(&named_field);
if path_ends_with_complex(&field.ty, "Base") {
is_base = true;
}
if path_ends_with_complex(&field.ty, "OnReady") {
field.is_onready = true;
}
if path_ends_with_complex(&field.ty, "OnEditor") {
field.is_oneditor = true;
}
if path_ends_with_complex(&field.ty, "PhantomVar") {
field.is_phantomvar = true;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "init")? {
if !matches!(init_strategy, InitStrategy::Generated) {
return bail!(
parser.span(),
"field attribute #[init] requires struct attribute #[class(init)]"
);
}
if let Some(default) = parser.handle_expr("val")? {
field.default_val = Some(FieldDefault {
default_val: default,
span: parser.span(),
});
}
if let Some((key, _default)) = parser.handle_expr_with_key("default")? {
return bail!(
key,
"#[init(default = ...)] has been renamed to #[init(val = ...)]",
);
}
if let Some(node_path) = parser.handle_expr("node")? {
field.set_default_val_if(
|| quote! { OnReady::from_node(#node_path) },
FieldCond::IsOnReady,
&parser,
&mut errors,
);
}
if let Some(resource_path) = parser.handle_expr("load")? {
field.set_default_val_if(
|| quote! { OnReady::from_loaded(#resource_path) },
FieldCond::IsOnReady,
&parser,
&mut errors,
);
}
if let Some(sentinel_value) = parser.handle_expr("sentinel")? {
field.set_default_val_if(
|| quote! { OnEditor::from_sentinel(#sentinel_value) },
FieldCond::IsOnEditor,
&parser,
&mut errors,
);
}
parser.finish()?;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export")? {
let export = FieldExport::new_from_kv(&mut parser)?;
field.export = Some(export);
parser.finish()?;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export_group")? {
let group = FieldGroup::new_from_kv(&mut parser)?;
field.group = Some(group);
parser.finish()?;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export_subgroup")? {
let subgroup = FieldGroup::new_from_kv(&mut parser)?;
field.subgroup = Some(subgroup);
parser.finish()?;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "var")? {
let var = FieldVar::new_from_kv(&mut parser)?;
if var.getter == GetterSetter::Disabled && var.setter == GetterSetter::Disabled {
return bail!(
var.span,
"#[var(no_get, no_set)] is not allowed; if you don't want a property, omit #[var] entirely"
);
}
if var.getter == GetterSetter::Disabled && field.export.is_some() {
return bail!(
var.span,
"#[export] with #[var(no_get)] is not supported; the editor requires a getter for serialization and inspector display"
);
}
field.var = Some(var);
parser.finish()?;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export_tool_button")? {
require_api_version!("4.4", parser.span(), "#[export_tool_button]")?;
if field.export.is_some() || field.var.is_some() {
return bail!(
parser.span(),
"`#[export_tool_button]` is mutually exclusive with `#[export]` and `#[var]`."
);
}
let var = FieldVar::new_tool_button_from_kv(&mut parser, &field.name)?;
field.var = Some(var);
field.default_val = Some(FieldDefault {
default_val: quote! { ::godot::register::property::PhantomVar::default() },
span: parser.span(),
});
has_tool_button = true;
parser.finish()?;
}
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "hint")? {
if let Some(override_base) = handle_opposite_keys(&mut parser, "base", "hint")? {
is_base = override_base;
}
if let Some(override_onready) = handle_opposite_keys(&mut parser, "onready", "hint")? {
field.is_onready = override_onready;
}
parser.finish()?;
}
if is_base {
validate_base_field(&field, &mut errors);
if let Some(prev_base) = base_field.replace(field) {
errors.push(error!(
named_field,
"at most 1 field can have type Base<T>; previous is `{}`", prev_base.name
));
}
} else {
if field.is_phantomvar {
validate_phantomvar_field(&field, &mut errors);
}
all_fields.push(field);
}
}
Ok(Fields {
all_fields,
base_field,
deprecations,
errors,
has_tool_button,
})
}
fn validate_base_field(field: &Field, errors: &mut Vec<Error>) {
if field.is_onready {
errors.push(error!(
field.ty.clone(),
"base field cannot have type `OnReady<T>`"
));
}
if let Some(var) = field.var.as_ref() {
errors.push(error!(
var.span,
"base field cannot have the attribute #[var]"
));
}
if let Some(export) = field.export.as_ref() {
errors.push(error!(
export.span,
"base field cannot have the attribute #[export]"
));
}
if let Some(default_val) = field.default_val.as_ref() {
errors.push(error!(
default_val.span,
"base field cannot have the attribute #[init]"
));
}
}
fn base_field_or_default(mut base: Option<Ident>, is_singleton: bool) -> Ident {
if let Some(base) = base.take() {
base
} else if is_singleton {
ident("Object")
} else {
ident("RefCounted")
}
}
fn validate_phantomvar_field(field: &Field, errors: &mut Vec<Error>) {
let Some(field_var) = &field.var else {
errors.push(error!(
field.span,
"PhantomVar<T> field is useless without attribute #[var]"
));
return;
};
match field_var.getter {
GetterSetter::Generated => {
errors.push(error!(
field_var.span,
"PhantomVar<T> stores no data, so it cannot use an autogenerated getter.\n\
use #[var(get, ...)] and provide get_fieldname() fn."
));
}
GetterSetter::ToolButton(_) | GetterSetter::Custom | GetterSetter::CustomRenamed(_) => {}
GetterSetter::Disabled => {
errors.push(error!(
field_var.span,
"PhantomVar<T> requires a custom getter"
));
}
}
match field_var.setter {
GetterSetter::Generated => {
errors.push(error!(
field_var.span,
"PhantomVar<T> stores no data, so it cannot use an autogenerated setter.\n\
use #[var(set, ...)] and provide set_fieldname() fn; or disable with #[var(no_set, ...)]."
));
}
GetterSetter::ToolButton(_) | GetterSetter::Custom | GetterSetter::CustomRenamed(_) => {}
GetterSetter::Disabled => {}
}
}
fn handle_opposite_keys(
parser: &mut KvParser,
key: &str,
attribute: &str,
) -> ParseResult<Option<bool>> {
let antikey = format!("no_{key}");
let result = handle_mutually_exclusive_keys(parser, attribute, &[key, &antikey])?;
if let Some(idx) = result {
Ok(Some(idx == 0))
} else {
Ok(None)
}
}
fn post_validate(base_ty: &Ident, is_tool: bool) -> ParseResult<()> {
let class_name = base_ty.to_string();
let is_class_extension = is_class_virtual_extension(&class_name);
let is_class_editor = is_class_editor_only(&class_name);
if is_class_extension && !is_tool {
return bail!(
base_ty,
"Base class `{}` is a virtual extension class, which runs in the editor and thus requires #[class(tool)].",
base_ty
);
} else if is_class_editor && !is_tool {
return bail!(
base_ty,
"Base class `{}` is an editor-only class and thus requires #[class(tool)].",
base_ty
);
}
Ok(())
}
fn is_class_virtual_extension(godot_class_name: &str) -> bool {
match godot_class_name {
"GDExtension" => false,
_ => godot_class_name.ends_with("Extension"),
}
}
fn is_class_editor_only(godot_class_name: &str) -> bool {
match godot_class_name {
"FileSystemDock" | "ScriptCreateDialog" | "ScriptEditor" | "ScriptEditorBase" => true,
_ => {
(godot_class_name.starts_with("ResourceImporter")
&& godot_class_name != "ResourceImporter")
|| godot_class_name.starts_with("Editor")
}
}
}