Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion example/scenes/tests_runner.tscn
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[gd_scene load_steps=4 format=3 uid="uid://5uvnhmsmksj8"]
[gd_scene load_steps=5 format=3 uid="uid://5uvnhmsmksj8"]

[ext_resource type="Script" path="res://scripts/tests/tests_runner.gd" id="1_runner"]
[ext_resource type="Script" path="res://scripts/tests/signal_test.ts" id="2_signalts"]
[ext_resource type="Script" path="res://scripts/tests/runtime_integration_test.ts" id="3_runtimets"]
[ext_resource type="Script" path="res://scripts/tests/runtime_inheritance_test.ts" id="4_inheritancets"]

[node name="TestsRunner" type="Node" unique_id=1924444567]
script = ExtResource("1_runner")
Expand All @@ -12,3 +13,6 @@ script = ExtResource("2_signalts")

[node name="RuntimeIntegrationTest" type="Node" parent="." unique_id=1931083819]
script = ExtResource("3_runtimets")

[node name="RuntimeInheritanceTest" type="Node" parent="."]
script = ExtResource("4_inheritancets")
31 changes: 31 additions & 0 deletions example/scripts/tests/runtime_inheritance_external_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import RuntimeIntegrationBase from "./runtime_base_test.js";

export interface ExternalRow {
title: string;
enabled: boolean;
}

interface ExternalNestedSettings {
count: number;
}

interface ExternalSettings {
label: string;
nested: ExternalNestedSettings;
}

export abstract class RuntimeInheritanceExternalBase extends RuntimeIntegrationBase {
@Export()
external_count: number = 19;

@Export()
external_dynamic: number = 18;

@Export()
external_rows: ExternalRow[] = [];

@Export()
external_settings: ExternalSettings = { label: "external", nested: { count: 4 } };
}

export default RuntimeInheritanceExternalBase;
209 changes: 209 additions & 0 deletions example/scripts/tests/runtime_inheritance_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import nodeAssert from "node:assert/strict";
import { ClassDB, GodotObject, PropertyHint, Resource, type VariantArgument } from "godot";
import * as InheritanceBaseModule from "./runtime_inheritance_external_base.js";

interface InheritedRow {
label: string;
count: number;
}

interface DerivedRow {
label: string;
count: number;
tag: string;
}

interface InheritedSettings {
label: string;
count: number;
}

function Observe(_target: object, _property: string): void {}
function ExportIgnored(_target: object, _property: string): void {}

function computedDefault(value: number): number {
return value;
}

abstract class RuntimeInheritanceAbstractRoot extends InheritanceBaseModule.RuntimeInheritanceExternalBase {
@Export()
abstract_root_value: number = 20;
}

class RuntimeInheritanceGrandparent extends RuntimeInheritanceAbstractRoot {
@Export({ hint: PropertyHint.PROPERTY_HINT_RANGE, hint_string: "0,100,1" })
overridden_count: number = 11;

@Export()
initializer_override: number = 12;

@Export()
leaf_override: number = 13;

@Export()
opaque_count: number = 14;
}

export abstract class RuntimeInheritanceParent extends RuntimeInheritanceGrandparent {
static exports: ExportMap = {
inherited_static: { type: "int", hint: 1, hint_string: "0,20,1", default: 7 },
inherited_static_initializer: { type: "int" },
};

inherited_static: number = 7;
inherited_static_initializer: number = 9;

@Export({ hint: PropertyHint.PROPERTY_HINT_RANGE, hint_string: "0,50,1" })
override overridden_count: number = 23;

override initializer_override: number = 24;
override opaque_count: number = computedDefault(44);

@Export()
override leaf_override: number = 25;

@Observe
@Export(PropertyHint.PROPERTY_HINT_RANGE, "0,10,0.5")
inherited_range: number = 5;

@Export({ hint: PropertyHint.PROPERTY_HINT_RESOURCE_TYPE, hint_string: "Resource" })
inherited_resource: Resource | null = null;

@Export()
inherited_rows: InheritedRow[] = [];

@Export()
overridden_rows: InheritedRow[] = [];

@Export()
inherited_settings: InheritedSettings = { label: "parent", count: 6 };

@ExportIgnored
inherited_not_exported: number = 5;
}

export default class RuntimeInheritanceTest extends RuntimeInheritanceParent {
static signals = {
test_finished: [
{ name: "success", type: "bool" },
{ name: "message", type: "String" },
],
} as const;

static exports: ExportMap = {
direct_static: { type: "int", default: 3 },
inherited_count: { type: "float", hint: 1, hint_string: "0,100,1" },
inherited_static: { type: "int", hint: 1, hint_string: "0,30,1" },
inherited_rows: { type: "Array", hint: 31, hint_string: "24/17:TypeScriptInterfaceResource" },
external_rows: { type: "Array", hint: 31, hint_string: "24/17:TypeScriptInterfaceResource" },
explicit_default: { type: "float", default: 99 },
};

direct_static: number = 3;
explicit_default: number = computedDefault(27);
override leaf_override: number = 37;
override external_dynamic: number = computedDefault(38);
override inherited_settings: InheritedSettings = { label: "leaf", count: 8 };

@Export()
override overridden_rows: DerivedRow[] = [];

@ExportIgnored
direct_not_exported: number = 6;

run_test(): void {
try {
const properties = this.get_property_list() as Array<{
name: VariantArgument;
type: VariantArgument;
hint: VariantArgument;
hint_string: VariantArgument;
usage: VariantArgument;
}>;
const property = (name: string) => {
const matches = properties.filter(entry => String(entry.name) === name);
nodeAssert.equal(matches.length, 1, `${name} must be exported exactly once`);
return matches[0];
};
const assertHint = (name: string, hint: number | bigint, hintString: string) => {
const metadata = property(name);
nodeAssert.equal(Number(metadata.hint), Number(hint), `${name} hint`);
nodeAssert.equal(String(metadata.hint_string), hintString, `${name} hint string`);
};
assertHint("inherited_range", PropertyHint.PROPERTY_HINT_RANGE, "0,10,0.5");
assertHint("overridden_count", PropertyHint.PROPERTY_HINT_RANGE, "0,50,1");
assertHint("inherited_static", PropertyHint.PROPERTY_HINT_RANGE, "0,30,1");
assertHint("inherited_count", PropertyHint.PROPERTY_HINT_RANGE, "0,100,1");
assertHint("inherited_label", 20, "base label");
assertHint("inherited_resource", PropertyHint.PROPERTY_HINT_RESOURCE_TYPE, "Resource");
nodeAssert.equal(Number(property("inherited_resource").type), 24);
assertHint("inherited_rows", PropertyHint.PROPERTY_HINT_ARRAY_TYPE, "24/17:TypeScriptInterfaceResource");
assertHint("external_rows", PropertyHint.PROPERTY_HINT_ARRAY_TYPE, "24/17:TypeScriptInterfaceResource");
assertHint("overridden_rows", PropertyHint.PROPERTY_HINT_ARRAY_TYPE, "24/17:TypeScriptInterfaceResource");
const propertyNames = properties.map(entry => String(entry.name));
nodeAssert.ok(!propertyNames.includes("inherited_not_exported"));
nodeAssert.ok(!propertyNames.includes("direct_not_exported"));
nodeAssert.ok(propertyNames.indexOf("direct_static") < propertyNames.indexOf("inherited_range"));
nodeAssert.ok(propertyNames.indexOf("inherited_range") < propertyNames.indexOf("external_count"));

for (const [name, expected] of Object.entries({
overridden_count: 23,
initializer_override: 24,
leaf_override: 37,
inherited_range: 5,
inherited_static: 7,
inherited_static_initializer: 9,
direct_static: 3,
inherited_count: 11,
external_count: 19,
abstract_root_value: 20,
})) {
property(name);
nodeAssert.equal(this.get(name), expected, `${name} actual value`);
nodeAssert.equal(this.property_can_revert(name), true, `${name} can revert`);
nodeAssert.equal(this.property_get_revert(name), expected, `${name} default`);
}
nodeAssert.equal(this.property_get_revert("inherited_resource"), null);
nodeAssert.deepEqual(this.property_get_revert("inherited_rows"), []);
nodeAssert.deepEqual(this.property_get_revert("external_rows"), []);
nodeAssert.deepEqual(this.property_get_revert("overridden_rows"), []);
for (const [name, actual] of [["opaque_count", 44], ["external_dynamic", 38]] as const) {
property(name);
nodeAssert.equal(this.get(name), actual);
nodeAssert.equal(this.property_can_revert(name), false, `${name} must not use an ancestor's stale default`);
}
nodeAssert.equal(this.get("explicit_default"), 27);
nodeAssert.equal(this.property_get_revert("explicit_default"), 99);
property("inherited_settings::label");
property("inherited_settings::count");
nodeAssert.equal(this.get("inherited_settings::label"), "leaf");
nodeAssert.equal(this.property_get_revert("inherited_settings::label"), "leaf");
nodeAssert.equal(this.property_get_revert("inherited_settings::count"), 8);
property("external_settings::nested::count");
nodeAssert.equal(this.property_get_revert("external_settings::nested::count"), 4);
for (const [name, prefix, usage] of [
["InheritedSettings", "inherited_settings::", 64],
["ExternalSettings", "external_settings::", 64],
["ExternalNestedSettings", "external_settings::nested::", 256],
] as const) {
const group = property(name);
nodeAssert.equal(Number(group.usage), usage);
nodeAssert.equal(String(group.hint_string), prefix);
}

for (const [name, fieldName] of [["inherited_rows", "label"], ["external_rows", "title"], ["overridden_rows", "tag"]] as const) {
const row = ClassDB.instantiate("TypeScriptInterfaceResource") as GodotObject;
this.set(name, [row]);
const fields = row.get_property_list() as Array<{ name: VariantArgument }>;
nodeAssert.ok(fields.some(field => String(field.name) === fieldName), `${name} schema missing`);
row.set(fieldName, "inherited");
nodeAssert.equal(row.get(fieldName), "inherited");
this.set(name, []);
}
this.emit_signal("test_finished", true, "runtime inheritance test passed");
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
this.emit_signal("test_finished", false, message);
}
}
}
15 changes: 14 additions & 1 deletion example/scripts/tests/runtime_integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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");
Expand All @@ -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]);
Expand Down Expand Up @@ -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}`);
Expand Down
1 change: 1 addition & 0 deletions include/script/typescript_script.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<godot::StringName, godot::PropertyInfo> &get_exported_properties() const { return properties; }
const godot::Vector<godot::PropertyInfo> &get_property_list_ordered() const { return property_list; }
const godot::HashMap<godot::StringName, godot::StringName> &get_interface_array_schemas() const { return interface_array_schemas; }
const godot::HashMap<godot::StringName, godot::Variant> &get_property_defaults() const { return property_defaults; }
godot::StringName get_base_class_name() const {
compile();
Expand Down
11 changes: 8 additions & 3 deletions src/script/typescript_language.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,19 @@ 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 {};
}

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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading