From 8fe3965c213adbdeec54ff44571668e167d9adbc Mon Sep 17 00:00:00 2001 From: JustDooooIt <844730658@qq.com> Date: Fri, 11 Sep 2026 06:44:13 +0800 Subject: [PATCH 1/6] feat: add RuntimeSameFileExportBase class and enhance runtime integration tests --- .../scripts/tests/runtime_integration_test.ts | 15 +++++++- src/script/typescript_language.cpp | 11 ++++-- src/script/typescript_script.cpp | 37 +++++++++++++------ test/test_repository_integrity.py | 13 ++++++- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/example/scripts/tests/runtime_integration_test.ts b/example/scripts/tests/runtime_integration_test.ts index b2d0c2a..346b172 100644 --- a/example/scripts/tests/runtime_integration_test.ts +++ b/example/scripts/tests/runtime_integration_test.ts @@ -57,7 +57,15 @@ type GodeLoadEsm = (filepath: string, source: string) => Promise<{ [key: string] type GodeCompileEsm = (source: string, filepath: string) => Promise<{ [key: string]: VariantArgument }>; type RuntimeEditorStringEnum = "idle" | 'running' | null | "done"; -class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { +export abstract class RuntimeSameFileExportBase extends RuntimeBaseModule.RuntimeIntegrationBase { + @Export() + same_file_inherited_label: string = "same-file-base"; + + @Export() + same_file_inherited_count: number = 23; +} + +class RuntimeIntegrationTest extends RuntimeSameFileExportBase { static signals = { test_finished: [ { name: "success", type: "bool" }, @@ -357,6 +365,7 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { assert(this.property_can_revert("label"), "exported string property cannot revert"); assert(this.property_can_revert("inherited_label"), `inherited exported string property cannot revert; properties: ${propertyNames.join(", ")}`); + assert(this.property_can_revert("same_file_inherited_label"), `same-file inherited exported string property cannot revert; properties: ${propertyNames.join(", ")}`); assert(this.property_can_revert("spawn_offset"), "exported Vector3 property cannot revert"); assert(this.property_can_revert("resource_slot"), "exported Resource property cannot revert"); assert(this.property_can_revert("static_resource_default_first"), "static exported Resource property cannot revert"); @@ -370,6 +379,8 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { nodeAssert.equal(this.property_get_revert("label"), "runtime"); nodeAssert.equal(this.property_get_revert("inherited_label"), "base-runtime"); nodeAssert.equal(this.property_get_revert("inherited_count"), 11); + nodeAssert.equal(this.property_get_revert("same_file_inherited_label"), "same-file-base"); + nodeAssert.equal(this.property_get_revert("same_file_inherited_count"), 23); nodeAssert.equal(this.property_get_revert("resource_slot"), null); nodeAssert.equal(this.property_get_revert("static_resource_default_first"), null); nodeAssert.deepEqual(this.property_get_revert("static_number_array"), [3]); @@ -438,6 +449,8 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { "static_range_namespace", "inherited_label", "inherited_count", + "same_file_inherited_label", + "same_file_inherited_count", ]; for (const name of expectedExportProperties) { assert(propertyNames.includes(name), `exported property missing from property list: ${name}`); diff --git a/src/script/typescript_language.cpp b/src/script/typescript_language.cpp index 1a3736f..30dffe9 100644 --- a/src/script/typescript_language.cpp +++ b/src/script/typescript_language.cpp @@ -277,6 +277,11 @@ StringName default_exported_class_name_from_statement(TSNode export_statement, c return StringName(); } +bool is_class_declaration_node(TSNode node) { + const char *node_type = ts_node_type(node); + return strcmp(node_type, "class_declaration") == 0 || strcmp(node_type, "abstract_class_declaration") == 0; +} + TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_count, const std::string &source, const StringName &name) { if (name.is_empty()) { return {}; @@ -284,7 +289,7 @@ TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_count, co for (uint32_t i = 0; i < child_count; i++) { TSNode child = ts_node_child(root_node, i); - if (strcmp(ts_node_type(child), "class_declaration") == 0) { + if (is_class_declaration_node(child)) { if (class_name_from_class_node(child, source) == name) { return child; } @@ -295,7 +300,7 @@ TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_count, co } for (uint32_t j = 0; j < ts_node_child_count(child); j++) { TSNode exported_child = ts_node_child(child, j); - if (strcmp(ts_node_type(exported_child), "class_declaration") == 0 && class_name_from_class_node(exported_child, source) == name) { + if (is_class_declaration_node(exported_child) && class_name_from_class_node(exported_child, source) == name) { return exported_child; } } @@ -455,7 +460,7 @@ TSNode find_default_class(TSNode root_node, uint32_t child_count, const std::str TSNode node = ts_node_child(child, j); if (strcmp(ts_node_type(node), "default") == 0) { is_default = true; - } else if (strcmp(ts_node_type(node), "class_declaration") == 0 && is_default) { + } else if (is_class_declaration_node(node) && is_default) { return node; } } diff --git a/src/script/typescript_script.cpp b/src/script/typescript_script.cpp index 6c9cd18..00724ac 100644 --- a/src/script/typescript_script.cpp +++ b/src/script/typescript_script.cpp @@ -279,6 +279,11 @@ static StringName default_exported_class_name_from_statement(TSNode export_state return StringName(); } +static bool is_class_declaration_node(TSNode node) { + const char *node_type = ts_node_type(node); + return strcmp(node_type, "class_declaration") == 0 || strcmp(node_type, "abstract_class_declaration") == 0; +} + static TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_count, const std::string &source, const StringName &name) { if (name.is_empty()) { return {}; @@ -286,7 +291,7 @@ static TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_co for (uint32_t i = 0; i < child_count; i++) { TSNode child = ts_node_child(root_node, i); - if (strcmp(ts_node_type(child), "class_declaration") == 0) { + if (is_class_declaration_node(child)) { if (class_name_from_class_node(child, source) == name) { return child; } @@ -297,7 +302,7 @@ static TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_co } for (uint32_t j = 0; j < ts_node_child_count(child); j++) { TSNode exported_child = ts_node_child(child, j); - if (strcmp(ts_node_type(exported_child), "class_declaration") == 0 && class_name_from_class_node(exported_child, source) == name) { + if (is_class_declaration_node(exported_child) && class_name_from_class_node(exported_child, source) == name) { return exported_child; } } @@ -1641,6 +1646,20 @@ static void finalize_explicit_object_hint(PropertyInfo &property) { } } +static bool member_has_export_decorator(TSNode member, const std::string &source) { + for (uint32_t i = 0; i < ts_node_child_count(member); i++) { + TSNode child = ts_node_child(member, i); + if (strcmp(ts_node_type(child), "decorator") != 0) { + continue; + } + const std::string decorator_text = node_text(source, child); + if (decorator_text.rfind("@Export", 0) == 0) { + return true; + } + } + return false; +} + static void collect_parent_properties( const StringName &parent_name, const StringName &parent_qualifier, @@ -1662,12 +1681,12 @@ static void collect_parent_properties( if (strcmp(ts_node_type(child), "export_statement") == 0) { for (uint32_t j = 0; j < ts_node_child_count(child); j++) { TSNode en = ts_node_child(child, j); - if (strcmp(ts_node_type(en), "class_declaration") == 0) { + if (is_class_declaration_node(en)) { parent_node = en; break; } } - } else if (strcmp(ts_node_type(child), "class_declaration") == 0) { + } else if (is_class_declaration_node(child)) { parent_node = child; } if (!ts_node_is_null(parent_node)) { @@ -1690,13 +1709,7 @@ static void collect_parent_properties( if (strcmp(ts_node_type(field), "public_field_definition") != 0) { continue; } - TSNode deco = ts_node_child_by_field_name(field, "decorator", 9); - if (ts_node_is_null(deco)) { - continue; - } - uint32_t ds = ts_node_start_byte(deco); - uint32_t de = ts_node_end_byte(deco); - if (source.substr(ds, de - ds).find("@Export") == std::string::npos) { + if (!member_has_export_decorator(field, source)) { continue; } TSNode fname = ts_node_child_by_field_name(field, "name", 4); @@ -1941,7 +1954,7 @@ static TSNode find_default_class(TSNode root_node, uint32_t child_count, const s TSNode en = ts_node_child(child, j); if (strcmp(ts_node_type(en), "default") == 0) { is_default = true; - } else if (strcmp(ts_node_type(en), "class_declaration") == 0 && is_default) { + } else if (is_class_declaration_node(en) && is_default) { return en; } } diff --git a/test/test_repository_integrity.py b/test/test_repository_integrity.py index ee93d85..1c7358c 100644 --- a/test/test_repository_integrity.py +++ b/test/test_repository_integrity.py @@ -1033,6 +1033,8 @@ def test_typescript_language_editor_surface_is_not_stubbed(self): "default_exported_class_name_from_statement", "default_exported_name_from_clause", "find_class_declaration_by_name", + "is_class_declaration_node", + '"abstract_class_declaration"', "node_text_is_default", 'strcmp(node_type, "member_expression")', 'strcmp(node_type, "generic_type")', @@ -1177,6 +1179,9 @@ def test_typescript_metadata_parser_resolves_project_imports_like_compiler(self) "default_exported_class_name_from_statement", "default_exported_name_from_clause", "find_class_declaration_by_name", + "is_class_declaration_node", + '"abstract_class_declaration"', + "member_has_export_decorator", "node_text_is_default", "unwrap_metadata_expression", "qualifier_from_extends_node", @@ -1310,7 +1315,11 @@ def test_typescript_metadata_parser_resolves_project_imports_like_compiler(self) runtime_base = (ROOT / "example/scripts/tests/runtime_base_test.ts").read_text(encoding="utf-8") runtime_export_types = (ROOT / "example/scripts/tests/runtime_export_types.ts").read_text(encoding="utf-8") runtime_external_resource = (ROOT / "example/scripts/tests/runtime_external_resource.ts").read_text(encoding="utf-8") - self.assertIn("class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase", runtime_test) + self.assertIn("export abstract class RuntimeSameFileExportBase extends RuntimeBaseModule.RuntimeIntegrationBase", runtime_test) + self.assertIn("class RuntimeIntegrationTest extends RuntimeSameFileExportBase", runtime_test) + self.assertIn('same_file_inherited_label: string = "same-file-base";', runtime_test) + self.assertIn("same_file_inherited_count: number = 23;", runtime_test) + self.assertIn('this.property_can_revert("same_file_inherited_label")', runtime_test) self.assertIn("export default RuntimeIntegrationTest;", runtime_test) self.assertIn("static signals = {", runtime_test) self.assertIn("} as const;", runtime_test) @@ -3371,7 +3380,7 @@ def test_generated_dts_singletons_are_instances_not_constructors(self): self.assertIn(" type int = number | bigint;", globals_dts) self.assertIn(" type float = number;", globals_dts) self.assertNotIn("type int = number;\n", globals_dts) - self.assertIn(" function Signal(...args: any[]): any;", globals_dts) + self.assertNotIn(" function Signal(...args: any[]): any;", globals_dts) self.assertIn(" function Tool(target: object): void;", globals_dts) self.assertIn(" function Tool(): any;", globals_dts) self.assertIn(" function GlobalClass(target: object): void;", globals_dts) From 0cb9bb236b180a4bd48699694c7c39b312347895 Mon Sep 17 00:00:00 2001 From: JustDooooIt <844730658@qq.com> Date: Fri, 11 Sep 2026 16:10:15 +0800 Subject: [PATCH 2/6] fix: replace cast_to with dynamic_cast for safer type conversions in class_binding template --- generator/templates/class_binding.cpp.jinja2 | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/generator/templates/class_binding.cpp.jinja2 b/generator/templates/class_binding.cpp.jinja2 index 627aebb..f55c4fb 100644 --- a/generator/templates/class_binding.cpp.jinja2 +++ b/generator/templates/class_binding.cpp.jinja2 @@ -25,7 +25,7 @@ Napi::Object {{ class_name }}Binding::create_singleton(Napi::Env env, godot::{{ } Napi::Object {{ class_name }}Binding_create(Napi::Env env, godot::Object *instance) { - godot::{{ godot_class_name }} *typed_instance = godot::Object::cast_to(instance); + godot::{{ godot_class_name }} *typed_instance = dynamic_cast(instance); if (!typed_instance) { return Napi::Object(); } @@ -38,11 +38,11 @@ godot::Object *{{ class_name }}Binding_unwrap(const Napi::Object &obj) { void {{ class_name }}Binding_wrap(const Napi::Object &obj, godot::Object *instance) { {{ class_name }}Binding *binding = {{ class_name }}Binding::Unwrap(obj); - binding->instance = godot::Object::cast_to(instance); + binding->instance = dynamic_cast(instance); binding->internal_object_id = binding->instance ? binding->instance->get_instance_id() : 0; binding->owns_instance = false; binding->referenced_instance = false; - godot::RefCounted *ref = godot::Object::cast_to(binding->instance); + godot::RefCounted *ref = dynamic_cast(binding->instance); if (ref) { ref->reference(); binding->referenced_instance = true; @@ -184,7 +184,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { {{ class_name }}Binding::{{ class_name }}Binding(const Napi::CallbackInfo& info) : Napi::ObjectWrap<{{ class_name }}Binding>(info) { godot::Object *script_owner = gode::consume_script_instance_owner(); if (script_owner) { - instance = godot::Object::cast_to(script_owner); + instance = dynamic_cast(script_owner); if (!instance) { Napi::TypeError::New(info.Env(), "{{ js_class_name }} script owner is not compatible with {{ js_class_name }}").ThrowAsJavaScriptException(); owns_instance = false; @@ -202,7 +202,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { owns_instance = false; return; } - instance = godot::Object::cast_to(source_object); + instance = dynamic_cast(source_object); if (!instance) { Napi::TypeError::New(info.Env(), "{{ js_class_name }} constructor expected an object compatible with {{ js_class_name }}").ThrowAsJavaScriptException(); owns_instance = false; @@ -231,7 +231,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { internal_object_id = instance->get_instance_id(); {% if is_ref_counted %} if (!owns_instance) { - godot::RefCounted *ref = godot::Object::cast_to(instance); + godot::RefCounted *ref = dynamic_cast(instance); if (ref) { ref->reference(); referenced_instance = true; @@ -247,7 +247,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { if (internal_object_id != 0) { godot::Object* obj = godot::ObjectDB::get_instance(internal_object_id); if (obj) { - godot::RefCounted* ref = godot::Object::cast_to(obj); + godot::RefCounted* ref = dynamic_cast(obj); if (ref && (owns_instance || referenced_instance)) { if (ref->unreference()) { godot::memdelete(ref); @@ -259,7 +259,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { if (internal_object_id != 0) { godot::Object* obj = godot::ObjectDB::get_instance(internal_object_id); if (obj) { - godot::Node* node = godot::Object::cast_to(obj); + godot::Node* node = dynamic_cast(obj); if (owns_instance && node && !node->is_inside_tree()) { godot::memdelete(node); } @@ -270,7 +270,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { Napi::Value {{ class_name }}Binding::_to_string(const Napi::CallbackInfo& info) { Napi::Object js_this = info.This().As(); - godot::{{ godot_class_name }}* instance = godot::Object::cast_to(gode::unwrap_godot_object(js_this)); + godot::{{ godot_class_name }}* instance = dynamic_cast(gode::unwrap_godot_object(js_this)); if (!instance) { return Napi::String::New(info.Env(), "null"); } @@ -280,7 +280,7 @@ Napi::Value {{ class_name }}Binding::_to_string(const Napi::CallbackInfo& info) {% for signal in signals %} Napi::Value {{ class_name }}Binding::signal_{{ signal.name }}(const Napi::CallbackInfo& info) { Napi::Object js_this = info.This().As(); - godot::{{ godot_class_name }}* instance = godot::Object::cast_to(gode::unwrap_godot_object(js_this)); + godot::{{ godot_class_name }}* instance = dynamic_cast(gode::unwrap_godot_object(js_this)); if (!instance) { return info.Env().Null(); } @@ -293,7 +293,7 @@ Napi::Value {{ class_name }}Binding::signal_{{ signal.name }}(const Napi::Callba Napi::Value {{ class_name }}Binding::{{ method.name_cpp }}(const Napi::CallbackInfo& info) { {% if not method.is_static %} Napi::Object js_this = info.This().As(); - godot::{{ godot_class_name }}* instance = godot::Object::cast_to(gode::unwrap_godot_object(js_this)); + godot::{{ godot_class_name }}* instance = dynamic_cast(gode::unwrap_godot_object(js_this)); if (!instance) { Napi::Error::New(info.Env(), "Object is null or has been deleted in {{ js_class_name }}.{{ method.name }}").ThrowAsJavaScriptException(); return info.Env().Null(); From 1efe5a6bc44d126cd7f434a20ec87523d3d58fc4 Mon Sep 17 00:00:00 2001 From: moluopro Date: Tue, 15 Sep 2026 23:05:43 +0800 Subject: [PATCH 3/6] test: restore generated Signal declaration assertion The declaration generator still emits the global Signal function. Require that declaration so the integrity check matches the generated API. --- test/test_repository_integrity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_repository_integrity.py b/test/test_repository_integrity.py index 1c7358c..4b952fe 100644 --- a/test/test_repository_integrity.py +++ b/test/test_repository_integrity.py @@ -3380,7 +3380,7 @@ def test_generated_dts_singletons_are_instances_not_constructors(self): self.assertIn(" type int = number | bigint;", globals_dts) self.assertIn(" type float = number;", globals_dts) self.assertNotIn("type int = number;\n", globals_dts) - self.assertNotIn(" function Signal(...args: any[]): any;", globals_dts) + self.assertIn(" function Signal(...args: any[]): any;", globals_dts) self.assertIn(" function Tool(target: object): void;", globals_dts) self.assertIn(" function Tool(): any;", globals_dts) self.assertIn(" function GlobalClass(target: object): void;", globals_dts) From 9f0909af4d4912ef884246ca146ecb5b14bee7e2 Mon Sep 17 00:00:00 2001 From: moluopro Date: Tue, 15 Sep 2026 23:05:52 +0800 Subject: [PATCH 4/6] chore: retain Godot object casting in generated bindings Keep this PR focused on exported-property inheritance by retaining the existing Object::cast_to entry point. Both casting variants pass the available runtime checks; a separate correctness or performance benefit for replacing it has not been established. --- generator/templates/class_binding.cpp.jinja2 | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/generator/templates/class_binding.cpp.jinja2 b/generator/templates/class_binding.cpp.jinja2 index f55c4fb..627aebb 100644 --- a/generator/templates/class_binding.cpp.jinja2 +++ b/generator/templates/class_binding.cpp.jinja2 @@ -25,7 +25,7 @@ Napi::Object {{ class_name }}Binding::create_singleton(Napi::Env env, godot::{{ } Napi::Object {{ class_name }}Binding_create(Napi::Env env, godot::Object *instance) { - godot::{{ godot_class_name }} *typed_instance = dynamic_cast(instance); + godot::{{ godot_class_name }} *typed_instance = godot::Object::cast_to(instance); if (!typed_instance) { return Napi::Object(); } @@ -38,11 +38,11 @@ godot::Object *{{ class_name }}Binding_unwrap(const Napi::Object &obj) { void {{ class_name }}Binding_wrap(const Napi::Object &obj, godot::Object *instance) { {{ class_name }}Binding *binding = {{ class_name }}Binding::Unwrap(obj); - binding->instance = dynamic_cast(instance); + binding->instance = godot::Object::cast_to(instance); binding->internal_object_id = binding->instance ? binding->instance->get_instance_id() : 0; binding->owns_instance = false; binding->referenced_instance = false; - godot::RefCounted *ref = dynamic_cast(binding->instance); + godot::RefCounted *ref = godot::Object::cast_to(binding->instance); if (ref) { ref->reference(); binding->referenced_instance = true; @@ -184,7 +184,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { {{ class_name }}Binding::{{ class_name }}Binding(const Napi::CallbackInfo& info) : Napi::ObjectWrap<{{ class_name }}Binding>(info) { godot::Object *script_owner = gode::consume_script_instance_owner(); if (script_owner) { - instance = dynamic_cast(script_owner); + instance = godot::Object::cast_to(script_owner); if (!instance) { Napi::TypeError::New(info.Env(), "{{ js_class_name }} script owner is not compatible with {{ js_class_name }}").ThrowAsJavaScriptException(); owns_instance = false; @@ -202,7 +202,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { owns_instance = false; return; } - instance = dynamic_cast(source_object); + instance = godot::Object::cast_to(source_object); if (!instance) { Napi::TypeError::New(info.Env(), "{{ js_class_name }} constructor expected an object compatible with {{ js_class_name }}").ThrowAsJavaScriptException(); owns_instance = false; @@ -231,7 +231,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { internal_object_id = instance->get_instance_id(); {% if is_ref_counted %} if (!owns_instance) { - godot::RefCounted *ref = dynamic_cast(instance); + godot::RefCounted *ref = godot::Object::cast_to(instance); if (ref) { ref->reference(); referenced_instance = true; @@ -247,7 +247,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { if (internal_object_id != 0) { godot::Object* obj = godot::ObjectDB::get_instance(internal_object_id); if (obj) { - godot::RefCounted* ref = dynamic_cast(obj); + godot::RefCounted* ref = godot::Object::cast_to(obj); if (ref && (owns_instance || referenced_instance)) { if (ref->unreference()) { godot::memdelete(ref); @@ -259,7 +259,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { if (internal_object_id != 0) { godot::Object* obj = godot::ObjectDB::get_instance(internal_object_id); if (obj) { - godot::Node* node = dynamic_cast(obj); + godot::Node* node = godot::Object::cast_to(obj); if (owns_instance && node && !node->is_inside_tree()) { godot::memdelete(node); } @@ -270,7 +270,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { Napi::Value {{ class_name }}Binding::_to_string(const Napi::CallbackInfo& info) { Napi::Object js_this = info.This().As(); - godot::{{ godot_class_name }}* instance = dynamic_cast(gode::unwrap_godot_object(js_this)); + godot::{{ godot_class_name }}* instance = godot::Object::cast_to(gode::unwrap_godot_object(js_this)); if (!instance) { return Napi::String::New(info.Env(), "null"); } @@ -280,7 +280,7 @@ Napi::Value {{ class_name }}Binding::_to_string(const Napi::CallbackInfo& info) {% for signal in signals %} Napi::Value {{ class_name }}Binding::signal_{{ signal.name }}(const Napi::CallbackInfo& info) { Napi::Object js_this = info.This().As(); - godot::{{ godot_class_name }}* instance = dynamic_cast(gode::unwrap_godot_object(js_this)); + godot::{{ godot_class_name }}* instance = godot::Object::cast_to(gode::unwrap_godot_object(js_this)); if (!instance) { return info.Env().Null(); } @@ -293,7 +293,7 @@ Napi::Value {{ class_name }}Binding::signal_{{ signal.name }}(const Napi::Callba Napi::Value {{ class_name }}Binding::{{ method.name_cpp }}(const Napi::CallbackInfo& info) { {% if not method.is_static %} Napi::Object js_this = info.This().As(); - godot::{{ godot_class_name }}* instance = dynamic_cast(gode::unwrap_godot_object(js_this)); + godot::{{ godot_class_name }}* instance = godot::Object::cast_to(gode::unwrap_godot_object(js_this)); if (!instance) { Napi::Error::New(info.Env(), "Object is null or has been deleted in {{ js_class_name }}.{{ method.name }}").ThrowAsJavaScriptException(); return info.Env().Null(); From 270e35a6857f0506c1d9a8f487a0c3eaaed7f543 Mon Sep 17 00:00:00 2001 From: moluopro Date: Tue, 15 Sep 2026 23:05:59 +0800 Subject: [PATCH 5/6] fix: preserve exported property metadata across inheritance Reuse the full property parser for same-file parents and merge each parent before inheriting into its child. Preserve export hints, static exports, interface array schemas, and Inspector groups while respecting child overrides. Resolve inherited defaults separately from property descriptors, honor child field initializers, and avoid stale ancestor defaults for opaque initializers. Match the Export decorator by name without collecting parent methods, signals, or RPC metadata. --- include/script/typescript_script.h | 1 + src/script/typescript_script.cpp | 252 +++++++++++++++-------------- test/test_repository_integrity.py | 3 +- 3 files changed, 130 insertions(+), 126 deletions(-) diff --git a/include/script/typescript_script.h b/include/script/typescript_script.h index e5643fa..7399c83 100644 --- a/include/script/typescript_script.h +++ b/include/script/typescript_script.h @@ -70,6 +70,7 @@ class TypeScriptScript : public godot::ScriptExtension { godot::Variant call_static_method(const godot::StringName &p_method, const godot::Variant **p_args, int32_t p_argcount, GDExtensionCallError &r_error) const; const godot::HashMap &get_exported_properties() const { return properties; } const godot::Vector &get_property_list_ordered() const { return property_list; } + const godot::HashMap &get_interface_array_schemas() const { return interface_array_schemas; } const godot::HashMap &get_property_defaults() const { return property_defaults; } godot::StringName get_base_class_name() const { compile(); diff --git a/src/script/typescript_script.cpp b/src/script/typescript_script.cpp index 00724ac..59b320e 100644 --- a/src/script/typescript_script.cpp +++ b/src/script/typescript_script.cpp @@ -1646,133 +1646,108 @@ static void finalize_explicit_object_hint(PropertyInfo &property) { } } -static bool member_has_export_decorator(TSNode member, const std::string &source) { - for (uint32_t i = 0; i < ts_node_child_count(member); i++) { - TSNode child = ts_node_child(member, i); - if (strcmp(ts_node_type(child), "decorator") != 0) { - continue; - } - const std::string decorator_text = node_text(source, child); - if (decorator_text.rfind("@Export", 0) == 0) { - return true; - } - } - return false; -} +static void parse_class_members(TSNode class_node, const std::string &source, const String &file_path, TSNode root_node, uint32_t child_count, HashMap &properties, Vector &property_list, HashMap &interface_array_schemas, HashMap &property_defaults, HashMap &methods, HashMap &static_methods, HashMap &signals, HashMap &rpc_configs, HashMap &member_lines, const HashMap> &interfaces, bool properties_only = false); +static void parse_static_exports(TSNode class_node, const std::string &source, const String &file_path, TSNode root_node, uint32_t child_count, HashMap &properties, Vector &property_list, HashMap &property_defaults); +static HashSet parse_exported_field_defaults(TSNode class_node, const std::string &source, const HashMap &properties, HashMap &property_defaults); static void collect_parent_properties( - const StringName &parent_name, - const StringName &parent_qualifier, + TSNode class_node, const std::string &source, TSNode root_node, uint32_t child_count, const String &file_path, HashMap &properties, Vector &property_list, - HashMap &property_defaults) { + HashMap &interface_array_schemas, + HashMap &property_defaults, + const HashMap> &interfaces, + HashSet &visited_classes) { + TSNode base_node = extends_class_node_from_class(class_node); + if (ts_node_is_null(base_node)) { + return; + } + const StringName parent_name = class_name_from_extends_node(base_node, source); + const StringName parent_qualifier = qualifier_from_extends_node(base_node, source); if (parent_name.is_empty()) { return; } - if (parent_qualifier.is_empty()) { - for (uint32_t i = 0; i < child_count; i++) { - TSNode child = ts_node_child(root_node, i); - TSNode parent_node = { 0 }; - if (strcmp(ts_node_type(child), "export_statement") == 0) { - for (uint32_t j = 0; j < ts_node_child_count(child); j++) { - TSNode en = ts_node_child(child, j); - if (is_class_declaration_node(en)) { - parent_node = en; + auto inherit_properties = [&](const HashMap &parent_properties, + const Vector &parent_property_list, + const HashMap &parent_schemas, + const HashMap &parent_defaults) { + HashSet inherited_properties; + for (const KeyValue &entry : parent_properties) { + if (!properties.has(entry.key)) { + properties[entry.key] = entry.value; + inherited_properties.insert(entry.key); + } + } + // A metadata-only override can retain the parent's interface array schema. + for (const KeyValue &entry : parent_schemas) { + const PropertyInfo *property = properties.getptr(entry.key); + const PropertyInfo *parent_property = parent_properties.getptr(entry.key); + if (!interface_array_schemas.has(entry.key) && property && parent_property && + property->type == parent_property->type && property->hint == parent_property->hint && + property->hint_string == parent_property->hint_string && property->class_name == parent_property->class_name) { + interface_array_schemas[entry.key] = entry.value; + } + } + for (const PropertyInfo &property : parent_property_list) { + bool include = inherited_properties.has(property.name); + // Inspector groups describe following fields; they are not property-map entries. + if ((property.usage & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP)) != 0) { + include = false; + for (const StringName &name : inherited_properties) { + if (String(name).begins_with(property.hint_string)) { + include = true; break; } } - } else if (is_class_declaration_node(child)) { - parent_node = child; - } - if (!ts_node_is_null(parent_node)) { - TSNode pname = ts_node_child_by_field_name(parent_node, "name", 4); - if (!ts_node_is_null(pname)) { - uint32_t ps = ts_node_start_byte(pname); - uint32_t pe = ts_node_end_byte(pname); - if (source.substr(ps, pe - ps) == String(parent_name).utf8().get_data()) { - StringName grandparent; - StringName grandparent_qualifier; - TSNode grandparent_node = extends_class_node_from_class(parent_node); - if (!ts_node_is_null(grandparent_node)) { - grandparent = class_name_from_extends_node(grandparent_node, source); - grandparent_qualifier = qualifier_from_extends_node(grandparent_node, source); - } - collect_parent_properties(grandparent, grandparent_qualifier, source, root_node, child_count, file_path, properties, property_list, property_defaults); - TSNode pbody = ts_node_child_by_field_name(parent_node, "body", 4); - for (uint32_t j = 0; j < ts_node_child_count(pbody); j++) { - TSNode field = ts_node_child(pbody, j); - if (strcmp(ts_node_type(field), "public_field_definition") != 0) { - continue; - } - if (!member_has_export_decorator(field, source)) { - continue; - } - TSNode fname = ts_node_child_by_field_name(field, "name", 4); - if (ts_node_is_null(fname)) { - continue; - } - uint32_t ns = ts_node_start_byte(fname); - uint32_t ne = ts_node_end_byte(fname); - StringName prop_name(source.substr(ns, ne - ns).c_str()); - if (properties.has(prop_name)) { - continue; - } - PropertyInfo pi; - pi.name = prop_name; - pi.usage = PROPERTY_USAGE_DEFAULT; - TSNode ftype = ts_node_child_by_field_name(field, "type", 4); - if (!ts_node_is_null(ftype)) { - std::string type_str = type_text_from_annotation(ftype, source); - configure_property_type(pi, type_str, file_path, source, root_node, child_count); - } - properties[prop_name] = pi; - property_list.push_back(pi); - TSNode fvalue = ts_node_child_by_field_name(field, "value", 5); - if (!ts_node_is_null(fvalue)) { - Variant default_value; - if (parse_default_value(fvalue, source, pi.type, default_value)) { - property_defaults[prop_name] = default_value; - } - } - } - return; - } - } } + if (include) { + property_list.push_back(property); + } + } + // Initializers override inherited defaults even without a repeated @Export. + const HashSet initialized_fields = parse_exported_field_defaults(class_node, source, properties, property_defaults); + for (const KeyValue &entry : parent_defaults) { + const StringName field_name(String(entry.key).get_slice("::", 0)); + if (!property_defaults.has(entry.key) && !initialized_fields.has(field_name)) { + property_defaults[entry.key] = entry.value; + } + } + }; + + TSNode parent_node = parent_qualifier.is_empty() ? find_class_declaration_by_name(root_node, child_count, source, parent_name) : TSNode{}; + if (!ts_node_is_null(parent_node)) { + if (visited_classes.has(parent_name)) { + return; } + visited_classes.insert(parent_name); + HashMap parent_properties; + Vector parent_property_list; + HashMap parent_schemas; + HashMap parent_defaults; + HashMap parent_methods, parent_static_methods, parent_signals; + HashMap parent_rpc_configs; + HashMap parent_member_lines; + // Use the same parser as the default class, including hints and interface schemas. + parse_class_members(parent_node, source, file_path, root_node, child_count, parent_properties, parent_property_list, parent_schemas, parent_defaults, parent_methods, parent_static_methods, parent_signals, parent_rpc_configs, parent_member_lines, interfaces, true); + parse_static_exports(parent_node, source, file_path, root_node, child_count, parent_properties, parent_property_list, parent_defaults); + parse_exported_field_defaults(parent_node, source, parent_properties, parent_defaults); + collect_parent_properties(parent_node, source, root_node, child_count, file_path, parent_properties, parent_property_list, parent_schemas, parent_defaults, interfaces, visited_classes); + inherit_properties(parent_properties, parent_property_list, parent_schemas, parent_defaults); + return; } String ts_path = resolve_imported_class_path(file_path, source, root_node, child_count, parent_name, parent_qualifier); if (ts_path.is_empty()) { return; } - Ref