diff --git a/example/scripts/tests/runtime_integration_test.ts b/example/scripts/tests/runtime_integration_test.ts index c80c861..fff1709 100644 --- a/example/scripts/tests/runtime_integration_test.ts +++ b/example/scripts/tests/runtime_integration_test.ts @@ -5,7 +5,8 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import v8 from "node:v8"; import vm from "node:vm"; -import { Color, Engine, GD, GDArray, GDDictionary, GDString, GodotObject, Image, ImageTexture, Node, PackedInt32Array, PackedScene, PackedStringArray, PackedVector3Array, Resource, ResourceLoader, ResourceSaver, type VariantArgument, Vector2, Vector2i, Vector3 } from "godot"; +import * as GodotModule from "godot"; +import { Color, Engine, GD, GDArray, GDDictionary, GDString, GodotObject, Image, ImageTexture, Node, PackedInt32Array, PackedScene, PackedStringArray, PackedVector3Array, PropertyHint, PropertyHint as PropertyHintAlias, Resource, ResourceLoader, ResourceLoader_CacheMode, ResourceSaver, type VariantArgument, VariantType, Vector2, Vector2i, Vector3 } from "godot"; import cjsFixture, { makeCommonPayload } from "./commonjs_fixture.cjs"; import type RuntimeArrayResource from "./runtime_array_resource.js"; import type RuntimeExternalResource from "./runtime_external_resource.js"; @@ -26,6 +27,7 @@ const VARIANT_TYPE_DICTIONARY = 27; const VARIANT_TYPE_ARRAY = 28; const VARIANT_TYPE_OBJECT = 24; const PROPERTY_HINT_NONE = 0; +const PROPERTY_HINT_RANGE = 1; const PROPERTY_HINT_ENUM = 2; const PROPERTY_HINT_TYPE_STRING = 23; const PROPERTY_HINT_ARRAY_TYPE = 31; @@ -69,6 +71,8 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { "static_imported_resource_array": { "type": "RuntimeExportTypes.RuntimeImportedResourceArray", "default": [] as const }, "static_imported_generic_dictionary": { "type": "RuntimeImportedGenericDictionary", "default": {} as const }, "static_imported_generic_external_array": { "type": "RuntimeImportedGenericArray", "default": [] as const }, + "static_range_alias": { "type": "float", "hint": PropertyHintAlias.PROPERTY_HINT_RANGE, "hint_string": "0,8,0.5", "default": 2 as const }, + "static_range_namespace": { "type": "float", "hint": GodotModule.PropertyHint.PROPERTY_HINT_RANGE, "hint_string": "0,4,1", "default": 3 as const }, } satisfies ExportMap; label = "runtime" as string; @@ -84,6 +88,8 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { static_imported_resource_array = [] as RuntimeExportTypes.RuntimeImportedResourceArray; static_imported_generic_dictionary = {} as Record; static_imported_generic_external_array = new GDArray() as RuntimeImportedGenericArray; + static_range_alias = 2 as number; + static_range_namespace = 3 as number; @Export() resource_slot: Resource | null = null; @@ -166,6 +172,15 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { @Export({ hint: 20, hint_string: "manual enum hint" }) editor_explicit_hint_enum: "first" | "second" = "first"; + @Export({ hint: PropertyHint.PROPERTY_HINT_RANGE, hint_string: "0.1,16,0.1,or_greater" }) + editor_range: number = 1; + + @Export({ hint: PropertyHintAlias.PROPERTY_HINT_RANGE, hint_string: "0,8,0.5" }) + editor_range_alias: number = 2; + + @Export(GodotModule.PropertyHint.PROPERTY_HINT_RANGE, "0,4,1") + editor_range_namespace: number = 3; + run_test(): void { void this.run(); } @@ -187,6 +202,13 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { nodeAssert.equal(moduleMarker, "esm-runtime-helper"); nodeAssert.equal(path.posix.basename("res://scripts/tests/runtime_integration_test.ts"), "runtime_integration_test.ts"); nodeAssert.equal(ResourceLoader.exists(RUNTIME_ARRAY_RESOURCE_SCRIPT_PATH, "TypeScriptScript"), true); + nodeAssert.equal(Number(PropertyHint.PROPERTY_HINT_RANGE), PROPERTY_HINT_RANGE); + nodeAssert.equal(String(PropertyHint[PROPERTY_HINT_RANGE]), "PROPERTY_HINT_RANGE"); + nodeAssert.equal(Number(PropertyHintAlias.PROPERTY_HINT_RANGE), PROPERTY_HINT_RANGE); + nodeAssert.equal(Number(GodotModule.PropertyHint.PROPERTY_HINT_RANGE), PROPERTY_HINT_RANGE); + nodeAssert.equal(Number(VariantType.TYPE_FLOAT), VARIANT_TYPE_FLOAT); + nodeAssert.equal(Number(ResourceLoader_CacheMode.CACHE_MODE_REUSE), 1); + nodeAssert.equal(String(ResourceLoader_CacheMode[1]), "CACHE_MODE_REUSE"); const esmPayload = buildRuntimePayload("alpha"); nodeAssert.deepEqual(esmPayload.values, [1, 2, 3]); @@ -369,6 +391,9 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { "editor_mixed_union", "editor_mixed_object_union", "editor_explicit_hint_enum", + "editor_range", + "editor_range_alias", + "editor_range_namespace", "static_resource_default_first", "static_image", "static_number_array", @@ -378,6 +403,8 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { "static_imported_resource_array", "static_imported_generic_dictionary", "static_imported_generic_external_array", + "static_range_alias", + "static_range_namespace", "inherited_label", "inherited_count", ]; @@ -440,6 +467,26 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { nodeAssert.equal(Number(explicitHintEnumProperty.type), VARIANT_TYPE_STRING); nodeAssert.equal(Number(explicitHintEnumProperty.hint), 20); nodeAssert.equal(String(explicitHintEnumProperty.hint_string), "manual enum hint"); + const rangeProperty = getExportProperty("editor_range"); + nodeAssert.equal(Number(rangeProperty.type), VARIANT_TYPE_FLOAT); + nodeAssert.equal(Number(rangeProperty.hint), PROPERTY_HINT_RANGE); + nodeAssert.equal(String(rangeProperty.hint_string), "0.1,16,0.1,or_greater"); + const rangeAliasProperty = getExportProperty("editor_range_alias"); + nodeAssert.equal(Number(rangeAliasProperty.type), VARIANT_TYPE_FLOAT); + nodeAssert.equal(Number(rangeAliasProperty.hint), PROPERTY_HINT_RANGE); + nodeAssert.equal(String(rangeAliasProperty.hint_string), "0,8,0.5"); + const rangeNamespaceProperty = getExportProperty("editor_range_namespace"); + nodeAssert.equal(Number(rangeNamespaceProperty.type), VARIANT_TYPE_FLOAT); + nodeAssert.equal(Number(rangeNamespaceProperty.hint), PROPERTY_HINT_RANGE); + nodeAssert.equal(String(rangeNamespaceProperty.hint_string), "0,4,1"); + const staticRangeAliasProperty = getExportProperty("static_range_alias"); + nodeAssert.equal(Number(staticRangeAliasProperty.type), VARIANT_TYPE_FLOAT); + nodeAssert.equal(Number(staticRangeAliasProperty.hint), PROPERTY_HINT_RANGE); + nodeAssert.equal(String(staticRangeAliasProperty.hint_string), "0,8,0.5"); + const staticRangeNamespaceProperty = getExportProperty("static_range_namespace"); + nodeAssert.equal(Number(staticRangeNamespaceProperty.type), VARIANT_TYPE_FLOAT); + nodeAssert.equal(Number(staticRangeNamespaceProperty.hint), PROPERTY_HINT_RANGE); + nodeAssert.equal(String(staticRangeNamespaceProperty.hint_string), "0,4,1"); const exportedResource = new Resource(); exportedResource.resource_name = "PersistentRuntimeResource"; diff --git a/generator/dts_generator.py b/generator/dts_generator.py index 5f67157..bef2b8d 100644 --- a/generator/dts_generator.py +++ b/generator/dts_generator.py @@ -4,10 +4,14 @@ from .base_generator import CodeGenerator from .utils.api_data import load_extension_api_json from .utils.binding_policy import ( + GLOBAL_ENUM_ALIASES, builtin_operator_method_name, + global_enum_export_name, method_conflicts_with_builtin_member, resolve_property_accessor, + sanitize_ts_identifier, skipped_method_reason, + singleton_enum_export_name, ) from .utils.type_mappings import ( JS_CLASS_RENAME_MAP, @@ -36,27 +40,9 @@ # Builtin classes that map directly to JS primitives — skip class generation SKIP_BUILTINS = frozenset(['Nil', 'void', 'bool', 'int', 'float']) -# Global enums to skip — exported below under JS-friendly aliases. -SKIP_GLOBAL_ENUMS = frozenset(['Variant.Type', 'Variant.Operator']) -VARIANT_ENUM_ALIASES = { - 'Variant.Type': 'VariantType', - 'Variant.Operator': 'VariantOperator', -} - # Rename map: Godot name → JS/TS API name (avoids conflicts with JS built-ins) RENAME_MAP = JS_CLASS_RENAME_MAP -# Method/param names that are reserved in TypeScript/JS -TS_RESERVED = frozenset([ - 'constructor', 'delete', 'class', 'new', 'return', 'typeof', - 'void', 'function', 'var', 'let', 'const', 'if', 'else', - 'for', 'while', 'break', 'continue', 'switch', 'case', - 'default', 'import', 'export', 'from', 'extends', 'super', - 'this', 'static', 'in', 'of', 'instanceof', - 'throw', 'try', 'catch', 'finally', 'async', 'await', - 'yield', 'debugger', 'with', 'enum', -]) - UTILITY_RETURN_TYPE_OVERRIDES = { 'instance_from_id': 'GodotObject | null', 'typeof': 'VariantType', @@ -70,15 +56,12 @@ def sanitize_name(name: str) -> str: - name = name.replace('-', '_') - if name in TS_RESERVED: - return name + '_gd' - return name + return sanitize_ts_identifier(name) def member_name(name: str) -> str: name = name.replace('-', '_') - if name in TS_RESERVED: + if sanitize_ts_identifier(name) != name: return json.dumps(name) return name @@ -479,8 +462,7 @@ def _gen_class(self, cls_data: dict, indent: int, is_singleton: bool = False) -> enum, indent, export=True, - const=True, - name=f'{name}_{sanitize_name(enum["name"])}', + name=singleton_enum_export_name(name, enum["name"]), ) lines.append('') @@ -597,15 +579,18 @@ def _gen_utility_functions(self, api: dict, indent: int) -> list: self._append_unique_line(lines, seen, f'{ind}{ind}{name}({params}): {ret};') return lines - def _gen_variant_alias_enums(self, api: dict) -> list: + def _gen_global_enums(self, api: dict) -> list: global_enums = {enum['name']: enum for enum in api.get('global_enums', [])} - missing = sorted(name for name in VARIANT_ENUM_ALIASES if name not in global_enums) + missing = sorted(name for name in GLOBAL_ENUM_ALIASES if name not in global_enums) if missing: raise KeyError(f"extension_api.json missing Variant enum(s): {', '.join(missing)}") lines = [] - for source_name, alias_name in VARIANT_ENUM_ALIASES.items(): - lines += self._gen_enum(global_enums[source_name], indent=1, export=True, const=True, name=alias_name) + for enum in api.get('global_enums', []): + export_name = global_enum_export_name(enum['name']) + if not export_name: + continue + lines += self._gen_enum(enum, indent=1, export=True, name=export_name) lines.append('') return lines @@ -704,15 +689,7 @@ def _generate(self, api: dict) -> list: '', ] - # Global enums - for enum in api.get('global_enums', []): - if enum['name'] in SKIP_GLOBAL_ENUMS: - continue - lines += self._gen_enum(enum, indent=1, export=True, const=True) - lines.append('') - - # Variant enums are exported under JS-friendly aliases since VariantBinding is not generated. - lines += self._gen_variant_alias_enums(api) + lines += self._gen_global_enums(api) # Builtin classes (Vector2, Color, …) for cls in api.get('builtin_classes', []): diff --git a/generator/register_generator.py b/generator/register_generator.py index 348dbca..b4453a4 100644 --- a/generator/register_generator.py +++ b/generator/register_generator.py @@ -1,10 +1,19 @@ from .base_generator import CodeGenerator from .utils.api_data import load_extension_api_json +from .utils.binding_policy import global_enum_export_name, singleton_enum_export_name from .utils.string_utils import to_snake_case +from .utils.type_mappings import JS_CLASS_RENAME_MAP + +def enum_variable_name(name): + variable_name = to_snake_case(name) + while "__" in variable_name: + variable_name = variable_name.replace("__", "_") + return variable_name + class RegisterGenerator(CodeGenerator): def run(self): - api_data = load_extension_api_json(required_keys=("builtin_classes", "classes")) + api_data = load_extension_api_json(required_keys=("builtin_classes", "classes", "global_enums")) builtins = [] classes = [] @@ -35,9 +44,36 @@ def run(self): 'include': f"classes/{snake_name}_binding.gen.h" }) + global_enums = [] + for enum_def in api_data['global_enums']: + export_name = global_enum_export_name(enum_def['name']) + if not export_name: + continue + global_enums.append({ + 'name': export_name, + 'variable_name': enum_variable_name(export_name), + 'values': enum_def.get('values', []), + }) + + singleton_names = {s['name'] for s in api_data.get('singletons', [])} + singleton_enum_aliases = [] + for class_def in api_data['classes']: + class_name = class_def['name'] + if class_name not in singleton_names: + continue + owner_name = JS_CLASS_RENAME_MAP.get(class_name, class_name) + for enum_def in class_def.get('enums', []): + export_name = singleton_enum_export_name(owner_name, enum_def['name']) + singleton_enum_aliases.append({ + 'name': export_name, + 'variable_name': enum_variable_name(export_name), + 'values': enum_def.get('values', []), + }) + context = { 'builtins': builtins, - 'classes': [] # We don't want to register classes globally anymore + 'classes': [], # We don't want to register classes globally anymore + 'global_enums': global_enums, } # Generate Header @@ -63,7 +99,8 @@ def run(self): context = { 'classes': classes, - 'singletons': singletons + 'singletons': singletons, + 'singleton_enum_aliases': singleton_enum_aliases, } self.render('register_classes.h.jinja2', context, 'register_classes.gen.h', 'include_dir') self.render('register_classes.cpp.jinja2', context, 'register_classes.gen.cpp', 'src_dir') diff --git a/generator/templates/register_builtin.cpp.jinja2 b/generator/templates/register_builtin.cpp.jinja2 index 4f9be2d..a065806 100644 --- a/generator/templates/register_builtin.cpp.jinja2 +++ b/generator/templates/register_builtin.cpp.jinja2 @@ -1,4 +1,5 @@ #include "register_builtin.gen.h" +#include "runtime/value_convert.h" // Builtins {% for item in builtins %} @@ -12,6 +13,17 @@ void register_builtin(Napi::Env env, Napi::Object exports) { {% for item in builtins %} {{ item.class_name }}Binding::init(env, exports); {% endfor %} + + // Register global enums as runtime objects so they remain usable when + // TypeScript isolatedModules disables ambient const enum access. + {% for enum in global_enums %} + Napi::Object {{ enum.variable_name }} = Napi::Object::New(env); + {% for value in enum['values'] %} + {{ enum.variable_name }}.Set("{{ value['name'] }}", gode::godot_result_to_napi(env, {{ value['value'] }})); + {{ enum.variable_name }}.Set(Napi::Number::New(env, {{ value['value'] }}), Napi::String::New(env, "{{ value['name'] }}")); + {% endfor %} + exports.Set("{{ enum.name }}", {{ enum.variable_name }}); + {% endfor %} } void reset_builtin_references() { diff --git a/generator/templates/register_classes.cpp.jinja2 b/generator/templates/register_classes.cpp.jinja2 index 3dc4e09..3d03fdb 100644 --- a/generator/templates/register_classes.cpp.jinja2 +++ b/generator/templates/register_classes.cpp.jinja2 @@ -38,6 +38,17 @@ void register_classes(Napi::Env env, Napi::Object exports) { } {% endif %} {% endfor %} + + // Register singleton enum aliases as module-level runtime objects. The DTS + // exposes these aliases because singleton instances cannot host namespaces. + {% for enum in singleton_enum_aliases %} + Napi::Object {{ enum.variable_name }} = Napi::Object::New(env); + {% for value in enum['values'] %} + {{ enum.variable_name }}.Set("{{ value['name'] }}", gode::godot_result_to_napi(env, {{ value['value'] }})); + {{ enum.variable_name }}.Set(Napi::Number::New(env, {{ value['value'] }}), Napi::String::New(env, "{{ value['name'] }}")); + {% endfor %} + exports.Set("{{ enum.name }}", {{ enum.variable_name }}); + {% endfor %} } void reset_class_references() { diff --git a/generator/utils/binding_policy.py b/generator/utils/binding_policy.py index 2ded202..ddafb19 100644 --- a/generator/utils/binding_policy.py +++ b/generator/utils/binding_policy.py @@ -8,6 +8,23 @@ ("VideoStreamPlayback", "mix_audio"): (1,), } +TS_RESERVED_NAMES = frozenset([ + 'constructor', 'delete', 'class', 'new', 'return', 'typeof', + 'void', 'function', 'var', 'let', 'const', 'if', 'else', + 'for', 'while', 'break', 'continue', 'switch', 'case', + 'default', 'import', 'export', 'from', 'extends', 'super', + 'this', 'static', 'in', 'of', 'instanceof', + 'throw', 'try', 'catch', 'finally', 'async', 'await', + 'yield', 'debugger', 'with', 'enum', +]) + +# Godot exposes these as dotted global enum names. The JS/TS surface uses stable +# module-level aliases because dotted identifiers are not valid exports. +GLOBAL_ENUM_ALIASES = { + 'Variant.Type': 'VariantType', + 'Variant.Operator': 'VariantOperator', +} + VARIANT_OPERATOR_ENUM_NAMES = { '==': 'OP_EQUAL', '!=': 'OP_NOT_EQUAL', @@ -127,3 +144,20 @@ def variant_operator_enum_name(operator_symbol: str) -> Optional[str]: def method_bind_out_argument_indices(class_name: str, method: dict) -> Sequence[int]: return METHOD_BIND_OUT_ARGUMENTS.get((class_name, method.get("name", "")), ()) + + +def sanitize_ts_identifier(name: str) -> str: + name = name.replace('-', '_') + if name in TS_RESERVED_NAMES: + return name + '_gd' + return name + + +def global_enum_export_name(enum_name: str) -> Optional[str]: + if enum_name in GLOBAL_ENUM_ALIASES: + return GLOBAL_ENUM_ALIASES[enum_name] + return sanitize_ts_identifier(enum_name) + + +def singleton_enum_export_name(owner_name: str, enum_name: str) -> str: + return f'{sanitize_ts_identifier(owner_name)}_{sanitize_ts_identifier(enum_name)}' diff --git a/src/script/typescript_script.cpp b/src/script/typescript_script.cpp index a30a734..aef6ea0 100644 --- a/src/script/typescript_script.cpp +++ b/src/script/typescript_script.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -450,6 +451,84 @@ static TSNode import_clause_from_statement(TSNode import_statement) { return {}; } +static std::string import_source_from_statement(TSNode import_statement, const std::string &source) { + TSNode src = ts_node_child_by_field_name(import_statement, "source", 6); + if (ts_node_is_null(src)) { + return std::string(); + } + return strip_quotes(node_text(source, src)); +} + +static bool import_statement_is_type_only(TSNode import_statement, const std::string &source) { + std::string text = node_text(source, import_statement); + while (!text.empty() && std::isspace(static_cast(text.front()))) { + text.erase(text.begin()); + } + return text.rfind("import type", 0) == 0; +} + +static bool godot_named_import_resolves_symbol(const std::string &source, TSNode root_node, uint32_t child_count, const std::string &local_name, const char *expected_imported_name) { + if (local_name.empty()) { + return false; + } + const StringName local_name_string(local_name.c_str()); + for (uint32_t i = 0; i < child_count; i++) { + TSNode child = ts_node_child(root_node, i); + if (strcmp(ts_node_type(child), "import_statement") != 0 || + import_statement_is_type_only(child, source) || + import_source_from_statement(child, source) != "godot") { + continue; + } + TSNode clause = import_clause_from_statement(child); + if (ts_node_is_null(clause)) { + continue; + } + for (uint32_t j = 0; j < ts_node_named_child_count(clause); j++) { + TSNode clause_child = ts_node_named_child(clause, j); + if (strcmp(ts_node_type(clause_child), "named_imports") != 0) { + continue; + } + for (uint32_t k = 0; k < ts_node_named_child_count(clause_child); k++) { + TSNode imported = ts_node_named_child(clause_child, k); + StringName imported_name; + if (strcmp(ts_node_type(imported), "import_specifier") == 0 && + import_specifier_resolves_name(imported, source, local_name_string, imported_name) && + String(imported_name) == String(expected_imported_name)) { + return true; + } + } + } + } + return false; +} + +static bool godot_namespace_import_binds_name(const std::string &source, TSNode root_node, uint32_t child_count, const std::string &namespace_name) { + if (namespace_name.empty()) { + return false; + } + const StringName namespace_name_string(namespace_name.c_str()); + for (uint32_t i = 0; i < child_count; i++) { + TSNode child = ts_node_child(root_node, i); + if (strcmp(ts_node_type(child), "import_statement") != 0 || + import_statement_is_type_only(child, source) || + import_source_from_statement(child, source) != "godot") { + continue; + } + TSNode clause = import_clause_from_statement(child); + if (ts_node_is_null(clause)) { + continue; + } + for (uint32_t j = 0; j < ts_node_named_child_count(clause); j++) { + TSNode clause_child = ts_node_named_child(clause, j); + if (strcmp(ts_node_type(clause_child), "namespace_import") == 0 && + namespace_import_binds_qualifier(clause_child, source, namespace_name_string)) { + return true; + } + } + } + return false; +} + static ImportedSymbolResolution resolve_imported_symbol(const String &file_path, const std::string &source, TSNode root_node, uint32_t child_count, const StringName &local_name, const StringName &qualifier = StringName(), bool include_dts = false) { ImportedSymbolResolution resolution; if (local_name.is_empty()) { @@ -506,13 +585,7 @@ static ImportedSymbolResolution resolve_imported_symbol(const String &file_path, continue; } - TSNode src = ts_node_child_by_field_name(child, "source", 6); - if (ts_node_is_null(src)) { - continue; - } - uint32_t ss = ts_node_start_byte(src); - uint32_t se = ts_node_end_byte(src); - std::string import_path = source.substr(ss + 1, se - ss - 2); + std::string import_path = import_source_from_statement(child, source); resolution.path = resolve_imported_typescript_path(file_path, import_path, include_dts); resolution.imported_name = imported_name; resolution.default_import = default_import; @@ -2162,13 +2235,124 @@ static bool parse_int_metadata_value(TSNode value, const std::string &source, in return false; } -static bool parse_property_hint_value(TSNode value, const std::string &source, PropertyHint &r_hint) { - int parsed = 0; - if (!parse_int_metadata_value(value, source, parsed)) { +static bool resolve_property_hint_member(const std::string &member_name, PropertyHint &r_hint) { +#define MATCH_PROPERTY_HINT(m_hint) \ + if (member_name == #m_hint) { \ + r_hint = m_hint; \ + return true; \ + } + MATCH_PROPERTY_HINT(PROPERTY_HINT_NONE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_RANGE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_ENUM) + MATCH_PROPERTY_HINT(PROPERTY_HINT_ENUM_SUGGESTION) + MATCH_PROPERTY_HINT(PROPERTY_HINT_EXP_EASING) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LINK) + MATCH_PROPERTY_HINT(PROPERTY_HINT_FLAGS) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_2D_RENDER) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_2D_PHYSICS) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_2D_NAVIGATION) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_3D_RENDER) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_3D_PHYSICS) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_3D_NAVIGATION) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LAYERS_AVOIDANCE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_FILE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_DIR) + MATCH_PROPERTY_HINT(PROPERTY_HINT_GLOBAL_FILE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_GLOBAL_DIR) + MATCH_PROPERTY_HINT(PROPERTY_HINT_RESOURCE_TYPE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_MULTILINE_TEXT) + MATCH_PROPERTY_HINT(PROPERTY_HINT_EXPRESSION) + MATCH_PROPERTY_HINT(PROPERTY_HINT_PLACEHOLDER_TEXT) + MATCH_PROPERTY_HINT(PROPERTY_HINT_COLOR_NO_ALPHA) + MATCH_PROPERTY_HINT(PROPERTY_HINT_OBJECT_ID) + MATCH_PROPERTY_HINT(PROPERTY_HINT_TYPE_STRING) + MATCH_PROPERTY_HINT(PROPERTY_HINT_NODE_PATH_TO_EDITED_NODE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_OBJECT_TOO_BIG) + MATCH_PROPERTY_HINT(PROPERTY_HINT_NODE_PATH_VALID_TYPES) + MATCH_PROPERTY_HINT(PROPERTY_HINT_SAVE_FILE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_GLOBAL_SAVE_FILE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_INT_IS_OBJECTID) + MATCH_PROPERTY_HINT(PROPERTY_HINT_INT_IS_POINTER) + MATCH_PROPERTY_HINT(PROPERTY_HINT_ARRAY_TYPE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_DICTIONARY_TYPE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LOCALE_ID) + MATCH_PROPERTY_HINT(PROPERTY_HINT_LOCALIZABLE_STRING) + MATCH_PROPERTY_HINT(PROPERTY_HINT_NODE_TYPE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_HIDE_QUATERNION_EDIT) + MATCH_PROPERTY_HINT(PROPERTY_HINT_PASSWORD) + MATCH_PROPERTY_HINT(PROPERTY_HINT_TOOL_BUTTON) + MATCH_PROPERTY_HINT(PROPERTY_HINT_ONESHOT) + MATCH_PROPERTY_HINT(PROPERTY_HINT_GROUP_ENABLE) + MATCH_PROPERTY_HINT(PROPERTY_HINT_INPUT_NAME) + MATCH_PROPERTY_HINT(PROPERTY_HINT_FILE_PATH) + MATCH_PROPERTY_HINT(PROPERTY_HINT_MAX) +#undef MATCH_PROPERTY_HINT + return false; +} + +static std::vector split_member_expression_path(const std::string &expression) { + std::vector parts; + size_t start = 0; + while (start <= expression.size()) { + size_t end = expression.find('.', start); + std::string part = trim_type_text(expression.substr(start, end == std::string::npos ? std::string::npos : end - start)); + if (part.empty()) { + return {}; + } + parts.push_back(part); + if (end == std::string::npos) { + break; + } + start = end + 1; + } + return parts; +} + +static bool property_hint_member_from_expression(TSNode value, const std::string &source, TSNode root_node, uint32_t child_count, std::string &r_member_name) { + if (strcmp(ts_node_type(value), "member_expression") != 0) { return false; } - r_hint = static_cast(parsed); - return true; + + const std::vector path = split_member_expression_path(node_text(source, value)); + if (path.size() == 2) { + if (path[0] == "PropertyHint" || + godot_named_import_resolves_symbol(source, root_node, child_count, path[0], "PropertyHint")) { + r_member_name = path[1]; + return true; + } + return false; + } + + if (path.size() == 3 && + path[1] == "PropertyHint" && + godot_namespace_import_binds_name(source, root_node, child_count, path[0])) { + r_member_name = path[2]; + return true; + } + + return false; +} + +static bool parse_property_hint_value(TSNode value, const std::string &source, TSNode root_node, uint32_t child_count, PropertyHint &r_hint) { + value = unwrap_metadata_expression(value); + if (ts_node_is_null(value)) { + return false; + } + + if (strcmp(ts_node_type(value), "number") == 0) { + int parsed = 0; + if (!parse_non_negative_int(node_text(source, value), parsed)) { + return false; + } + r_hint = static_cast(parsed); + return true; + } + + std::string member_name; + if (!property_hint_member_from_expression(value, source, root_node, child_count, member_name)) { + return false; + } + return resolve_property_hint_member(member_name, r_hint); } static bool parse_metadata_string_value(TSNode value, const std::string &source, String &r_value) { @@ -2364,14 +2548,14 @@ static void parse_class_members(TSNode class_node, const std::string &source, co } std::string key_str = strip_quotes(source.substr(ts_node_start_byte(key), ts_node_end_byte(key) - ts_node_start_byte(key))); if (key_str == "hint") { - parse_property_hint_value(val, source, export_hint); + parse_property_hint_value(val, source, root_node, child_count, export_hint); } else if (key_str == "hint_string") { parse_metadata_string_value(val, source, export_hint_string); } } } else { // @Export(hint) or @Export(hint, "hint_string"). - parse_property_hint_value(first_arg, source, export_hint); + parse_property_hint_value(first_arg, source, root_node, child_count, export_hint); if (nargs >= 2) { TSNode second_arg = unwrap_metadata_expression(ts_node_named_child(args, 1)); parse_metadata_string_value(second_arg, source, export_hint_string); @@ -2611,7 +2795,7 @@ static void parse_exports_object(TSNode obj_node, const std::string &source, con type_str = strip_quotes(node_text(source, fval)); } else if (field_key == "hint") { PropertyHint parsed_hint = PROPERTY_HINT_NONE; - if (parse_property_hint_value(fval, source, parsed_hint)) { + if (parse_property_hint_value(fval, source, root_node, child_count, parsed_hint)) { pi.hint = parsed_hint; } } else if (field_key == "hint_string") { diff --git a/test/test_repository_integrity.py b/test/test_repository_integrity.py index 9a3870d..bcbc84b 100644 --- a/test/test_repository_integrity.py +++ b/test/test_repository_integrity.py @@ -534,6 +534,31 @@ def test_generated_static_napi_references_reset_before_node_environment_free(sel self.assertIn("constructor.Reset();", register_builtin_template) self.assertIn("constructor.Reset();", register_classes_template) + def test_generated_global_enums_have_runtime_exports(self): + builtin_source = (ROOT / "src/generated/register_builtin.gen.cpp").read_text(encoding="utf-8") + class_source = (ROOT / "src/generated/register_classes.gen.cpp").read_text(encoding="utf-8") + register_generator = (ROOT / "generator/register_generator.py").read_text(encoding="utf-8") + register_template = (ROOT / "generator/templates/register_builtin.cpp.jinja2").read_text(encoding="utf-8") + register_classes_template = (ROOT / "generator/templates/register_classes.cpp.jinja2").read_text(encoding="utf-8") + binding_policy = (ROOT / "generator/utils/binding_policy.py").read_text(encoding="utf-8") + godot_dts = (ROOT / "example/addons/gode/types/godot.d.ts").read_text(encoding="utf-8") + + self.assertIn("'global_enums': global_enums", register_generator) + self.assertIn("'singleton_enum_aliases': singleton_enum_aliases", register_generator) + self.assertIn("global_enum_export_name", binding_policy) + self.assertIn("singleton_enum_export_name", binding_policy) + self.assertNotIn("from .dts_generator import", register_generator) + self.assertIn('exports.Set("{{ enum.name }}", {{ enum.variable_name }});', register_template) + self.assertIn('exports.Set("{{ enum.name }}", {{ enum.variable_name }});', register_classes_template) + self.assertIn("gode::godot_result_to_napi", register_template) + for enum_name in ("PropertyHint", "VariantType", "VariantOperator"): + self.assertIn(f'exports.Set("{enum_name}"', builtin_source) + self.assertIn(f" export enum {enum_name} {{", godot_dts) + self.assertNotIn(f" export const enum {enum_name} {{", godot_dts) + self.assertIn('exports.Set("ResourceLoader_CacheMode"', class_source) + self.assertIn(" export enum ResourceLoader_CacheMode {", godot_dts) + self.assertNotIn(" export const enum ResourceLoader_CacheMode {", godot_dts) + def test_value_convert_registry_and_cache_are_restart_safe(self): header = (ROOT / "include/runtime/value_convert.h").read_text(encoding="utf-8") source = (ROOT / "src/runtime/value_convert.cpp").read_text(encoding="utf-8") @@ -2518,8 +2543,10 @@ def test_generated_dts_singletons_are_instances_not_constructors(self): self.assertIn(" function Signal(...args: any[]): any;", globals_dts) self.assertIn(" function Tool(...args: any[]): any;", globals_dts) self.assertNotIn("GodotModule.", globals_dts) - self.assertIn(" export const enum VariantType {", godot_dts) - self.assertNotIn(" export const VariantType: typeof VariantType;", godot_dts) + self.assertIn(" export enum VariantType {", godot_dts) + self.assertNotIn(" export const enum VariantType {", godot_dts) + self.assertIn(" export enum ResourceLoader_CacheMode {", godot_dts) + self.assertNotIn(" export const enum ResourceLoader_CacheMode {", godot_dts) self.assertIn(" export class PhysicsServer3DExtension extends __GodotSingletonBases.PhysicsServer3D {", godot_dts) def test_generated_dts_has_no_duplicate_class_member_declarations(self): @@ -2562,7 +2589,7 @@ def test_generated_variant_alias_enums_match_extension_api(self): def dts_enum_values(enum_name: str) -> list[tuple[str, int]]: match = re.search( - rf"^\s*export const enum {re.escape(enum_name)} \{{\n(?P.*?)^\s*\}}", + rf"^\s*export enum {re.escape(enum_name)} \{{\n(?P.*?)^\s*\}}", godot_dts, re.DOTALL | re.MULTILINE, )