From f7433c8bf50ad485076de7b6896f54edadc95491 Mon Sep 17 00:00:00 2001 From: Nicolas Merget Date: Sat, 20 Dec 2025 00:46:36 +0100 Subject: [PATCH 1/3] feat: replaced @bind.help/experimental/deprecated with standard JS comments and JSDoc like annotations --- .changeset/silent-clouds-matter.md | 41 ++ bridge/jsb_bridge_module_loader.cpp | 78 --- bridge/jsb_class_info.cpp | 73 +-- bridge/jsb_class_info.h | 29 - scripts/jsb.editor/src/jsb.editor.codegen.ts | 83 +-- scripts/jsb.runtime/src/godot.annotations.ts | 153 ----- scripts/jsb.runtime/src/jsb.core.ts | 48 -- scripts/typings/godot.generated.d.ts | 9 - scripts/typings/godot.minimal.d.ts | 9 - .../DocumentationComments.nodes.gen.ts | 5 + .../DocumentationComments.tscn.gen.ts | 6 + .../DocumentationComments.tscn | 11 + .../documentation-comments.ts | 74 +++ weaver/jsb_script.cpp | 544 +++++++++++++++++- 14 files changed, 658 insertions(+), 505 deletions(-) create mode 100644 .changeset/silent-clouds-matter.md create mode 100644 tests/project/gen/godot/tests/documentation-comments/DocumentationComments.nodes.gen.ts create mode 100644 tests/project/gen/godot/tests/documentation-comments/DocumentationComments.tscn.gen.ts create mode 100644 tests/project/tests/documentation-comments/DocumentationComments.tscn create mode 100644 tests/project/tests/documentation-comments/documentation-comments.ts diff --git a/.changeset/silent-clouds-matter.md b/.changeset/silent-clouds-matter.md new file mode 100644 index 00000000..64ed8bc8 --- /dev/null +++ b/.changeset/silent-clouds-matter.md @@ -0,0 +1,41 @@ +--- +"@godot-js/editor": major +--- + +feat: replaced @bind.help/experimental/deprecated with standard JS comments and JSDoc like annotations + +Here is an example for more information checkout https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_documentation_comments.html: +````ts +/** + * A brief description of the class's role and functionality. + * + * The description of the script, what it can do, + * and any further detail. + * + * @tutorial https://example.com/tutorial_1 + * @tutorial(Tutorial 2) https://example.com/tutorial_2 + * @experimental + */ +export default class DocumentationComments extends Node { + + /** + * This is a multiline description of the variable v2. + * The type information below will be extracted for the documentation. + */ + v2: number = 0; + + /** + * As the following function is documented, even though its name starts with + * an underscore, it will appear in the help window. + */ + _fn(p1: number, p2: string): number { + return 0; + } + + /** + * This function is deprecated and should not appear in the help window. + * @deprecated Use [method _fn] instead + */ + _deprecated() {} +} +```` \ No newline at end of file diff --git a/bridge/jsb_bridge_module_loader.cpp b/bridge/jsb_bridge_module_loader.cpp index 0c17333e..65619d12 100644 --- a/bridge/jsb_bridge_module_loader.cpp +++ b/bridge/jsb_bridge_module_loader.cpp @@ -272,83 +272,6 @@ namespace jsb impl::Helper::to_string_opt(isolate, evaluator->Get(context, jsb_name(environment, name)))); } - // function set_script_doc(target: GObjectConstructor, property_key: undefined, field: 0 | 1 | 2, message: string): void; - // function set_script_doc(target: GObject, property_key: string, field: 0 | 1 | 2, message: string): void; - void _set_script_doc(const v8::FunctionCallbackInfo& info) - { -#ifdef TOOLS_ENABLED - constexpr int kTarget = 0; // constructor | prototype - constexpr int kProperty = 1; // undefined | string - constexpr int kField = 2; // int32: 0, 1, 2 - constexpr int kMessage = 3; // undefined | string - - v8::Isolate* isolate = info.GetIsolate(); - v8::HandleScope handle_scope(isolate); - const v8::Local context = isolate->GetCurrentContext(); - if (info.Length() != 4 || !info[kTarget]->IsObject() || !info[kField]->IsNumber()) - { - jsb_throw(isolate, "bad param"); - return; - } - Environment* environment = Environment::wrap(isolate); - const v8::Local target = info[kTarget].As(); - const v8::Local property = info[kProperty].As(); - const ScriptClassDocField::Type doc_item = (ScriptClassDocField::Type) info[kField].As()->Value(); - const v8::Local message = info[kMessage]->IsString() ? info[kMessage].As() : v8::String::Empty(isolate); - v8::Local doc; - - // always set doc info on `prototype` - if (property->IsUndefined()) - { - // doc for class - const v8::Local prototype = target->Get(context, jsb_name(environment, prototype)).ToLocalChecked().As(); - jsb_check(prototype->IsObject()); - if (v8::Local val; !prototype->Get(context, jsb_symbol(environment, Doc)).ToLocal(&val) || !val->IsObject()) - { - doc = v8::Object::New(isolate); - prototype->Set(context, jsb_symbol(environment, Doc), doc).Check(); - } - else - { - doc = val.As(); - } - } - else - { - // doc for member - jsb_check(property->IsString() && property.As()->Length() != 0); - v8::Local member_doc_map; - if (v8::Local val; !target->Get(context, jsb_symbol(environment, MemberDocMap)).ToLocal(&val) || !val->IsMap()) - { - member_doc_map = v8::Map::New(isolate); - target->Set(context, jsb_symbol(environment, MemberDocMap), member_doc_map).Check(); - } - else - { - member_doc_map = val.As(); - } - - if (v8::Local val; !member_doc_map->Get(context, property).ToLocal(&val) || !val->IsObject()) - { - doc = v8::Object::New(isolate); - jsb_ensure(!member_doc_map->Set(context, property, doc).IsEmpty()); - } - else - { - doc = val.As(); - } - } - - switch (doc_item) - { - case ScriptClassDocField::Deprecated: doc->Set(context, jsb_name(environment, deprecated), message).Check(); return; - case ScriptClassDocField::Experimental: doc->Set(context, jsb_name(environment, experimental), message).Check(); return; - case ScriptClassDocField::Help: doc->Set(context, jsb_name(environment, help), message).Check(); return; - } - jsb_throw(isolate, "bad param"); -#endif - } - // function add_script_property(prototype: GObject, details: ScriptPropertyInfo): void void _add_script_property(const v8::FunctionCallbackInfo& info) { @@ -581,7 +504,6 @@ namespace jsb internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "add_script_tool"), JSB_NEW_FUNCTION(context, _add_script_tool, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "add_script_icon"), JSB_NEW_FUNCTION(context, _add_script_icon, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "add_script_rpc"), JSB_NEW_FUNCTION(context, _add_script_rpc, {})).Check(); - internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "set_script_doc"), JSB_NEW_FUNCTION(context, _set_script_doc, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "notify_microtasks_run"), JSB_NEW_FUNCTION(context, _notify_microtasks_run, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "create_script_cached_property_updater"), JSB_NEW_FUNCTION(context, _create_script_cached_property_updater, {})).Check(); diff --git a/bridge/jsb_class_info.cpp b/bridge/jsb_class_info.cpp index 7d0265a2..1f34090c 100644 --- a/bridge/jsb_class_info.cpp +++ b/bridge/jsb_class_info.cpp @@ -7,52 +7,6 @@ namespace jsb { -#ifdef TOOLS_ENABLED - void _parse_script_doc(v8::Isolate* isolate, const v8::Local& context, - const v8::MaybeLocal holder, ScriptBaseDoc& r_doc) - { - if (v8::Local tv; holder.IsEmpty() || !holder.ToLocal(&tv) || !tv->IsObject()) - { - // invalid - } - else - { - const v8::Local obj = tv.As(); - Environment* environment = Environment::wrap(isolate); - - // (@deprecated) - if (v8::Local val; obj->Get(context, jsb_name(environment, deprecated)).ToLocal(&val) && val->IsString()) - { - r_doc.is_deprecated = true; - r_doc.deprecated_message = impl::Helper::to_string(isolate, val); - } - else - { - r_doc.is_deprecated = false; - } - // (@experimental) - if (v8::Local val; obj->Get(context, jsb_name(environment, experimental)).ToLocal(&val) && val->IsString()) - { - r_doc.is_experimental = true; - r_doc.experimental_message = impl::Helper::to_string(isolate, val); - } - else - { - r_doc.is_experimental = false; - } - // (@help) - if (v8::Local val; obj->Get(context, jsb_name(environment, help)).ToLocal(&val) && val->IsString()) - { - r_doc.brief_description = impl::Helper::to_string(isolate, val); - } - else - { - r_doc.brief_description.clear(); - } - } - } -#endif - //NOTE ensure the address of p_class_info being locked during this procedure void _parse_script_class_iterate(const v8::Local& p_context, const ScriptClassInfoPtr& p_class_info, const v8::Local& class_obj) { @@ -80,16 +34,6 @@ namespace jsb JSB_LOG(VeryVerbose, "godot js class name %s (native: %s)", p_class_info->js_class_name, p_class_info->native_class_name); -#ifdef TOOLS_ENABLED - // class doc - v8::Local doc_map; - if (v8::Local val; prototype->Get(p_context, jsb_symbol(environment, MemberDocMap)).ToLocal(&val) && val->IsMap()) - { - doc_map = val.As(); - } - _parse_script_doc(isolate, p_context, prototype->Get(p_context, jsb_symbol(environment, Doc)), p_class_info->doc); -#endif - // class rpc config v8::Local rpc_config_map; if (v8::Local val; prototype->Get(p_context, jsb_symbol(environment, ClassRPCConfig)).ToLocal(&val) && val->IsMap()) @@ -117,15 +61,8 @@ namespace jsb v8::Local prop_val; if (prop_descriptor.As()->Get(p_context, jsb_name(environment, value)).ToLocal(&prop_val) && prop_val->IsFunction()) { - //TODO property categories - ScriptMethodInfo method_info = {}; -#ifdef TOOLS_ENABLED - if (v8::Local val; !doc_map.IsEmpty() && doc_map->Get(p_context, prop_name).ToLocal(&val) && val->IsObject()) - { - _parse_script_doc(isolate, p_context, val, method_info.doc); - } -#endif // TOOLS_ENABLED - p_class_info->methods.insert((StringName) name_s, method_info); + + p_class_info->methods.insert((StringName) name_s, {}); // check rpc config if (v8::Local rpc_val; @@ -231,12 +168,6 @@ namespace jsb property_info.cache = cache->BooleanValue(isolate); } -#ifdef TOOLS_ENABLED - if (v8::Local val; !doc_map.IsEmpty() && doc_map->Get(p_context, prop_name).ToLocal(&val) && val->IsObject()) - { - _parse_script_doc(isolate, p_context, val, property_info.doc); - } -#endif // TOOLS_ENABLED p_class_info->properties.insert(property_info.name, property_info); JSB_LOG(VeryVerbose, "... property %s: %s", property_info.name, Variant::get_type_name(property_info.type)); } diff --git a/bridge/jsb_class_info.h b/bridge/jsb_class_info.h index f5b0b6a6..0d54b5c4 100644 --- a/bridge/jsb_class_info.h +++ b/bridge/jsb_class_info.h @@ -93,27 +93,6 @@ namespace jsb }; } -#ifdef TOOLS_ENABLED - struct ScriptBaseDoc - { - String brief_description; - - String deprecated_message; - String experimental_message; - - bool is_deprecated = false; - bool is_experimental = false; - - }; - - struct ScriptClassDoc : ScriptBaseDoc {}; - struct ScriptMethodDoc : ScriptBaseDoc {}; - struct ScriptPropertyDoc : ScriptBaseDoc {}; -#else - struct ScriptClassDoc {}; - struct ScriptMethodDoc {}; - struct ScriptPropertyDoc {}; -#endif namespace ScriptMethodFlags { @@ -130,9 +109,6 @@ namespace jsb struct ScriptMethodInfo { - // only valid with TOOLS_ENABLED - ScriptMethodDoc doc; - ScriptMethodFlags::Type flags = ScriptMethodFlags::None; // v8::Global cache_; @@ -154,8 +130,6 @@ namespace jsb String hint_string; - ScriptPropertyDoc doc; - // valid only if _Evaluated flag is set in ScriptClassInfo.flags Variant default_value; @@ -201,9 +175,6 @@ namespace jsb // script icon path for showing in scene hierarchy String icon; - // only valid with TOOLS_ENABLED - ScriptClassDoc doc; - Dictionary rpc_config; HashMap methods; diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index 0a5eb934..24aaef3a 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -1888,88 +1888,7 @@ const annotation_types = { }, ], }, - }, - deprecated: { - type: DescriptorType.FunctionLiteral, - parameters: [ - { name: "message", type: { type: DescriptorType.Godot, name: "string" }, optional: true }, - ], - returns: { - type: DescriptorType.Godot, - name: "Decorator", - arguments: [ - { - type: DescriptorType.Union, - types: [ - { - type: DescriptorType.Godot, - name: "ClassDecoratorContext", - arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], - }, - { - type: DescriptorType.Godot, - name: "ClassValueMemberDecoratorContext", - arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], - }, - ], - }, - ], - }, - }, - experimental: { - type: DescriptorType.FunctionLiteral, - parameters: [ - { name: "message", type: { type: DescriptorType.Godot, name: "string" }, optional: true }, - ], - returns: { - type: DescriptorType.Godot, - name: "Decorator", - arguments: [ - { - type: DescriptorType.Union, - types: [ - { - type: DescriptorType.Godot, - name: "ClassDecoratorContext", - arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], - }, - { - type: DescriptorType.Godot, - name: "ClassValueMemberDecoratorContext", - arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], - }, - ], - }, - ], - }, - }, - help: { - type: DescriptorType.FunctionLiteral, - parameters: [ - { name: "message", type: { type: DescriptorType.Godot, name: "string" }, optional: true }, - ], - returns: { - type: DescriptorType.Godot, - name: "Decorator", - arguments: [ - { - type: DescriptorType.Union, - types: [ - { - type: DescriptorType.Godot, - name: "ClassDecoratorContext", - arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], - }, - { - type: DescriptorType.Godot, - name: "ClassValueMemberDecoratorContext", - arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], - }, - ], - }, - ], - }, - }, + } }, }, ], diff --git a/scripts/jsb.runtime/src/godot.annotations.ts b/scripts/jsb.runtime/src/godot.annotations.ts index 1221f4e2..235df317 100644 --- a/scripts/jsb.runtime/src/godot.annotations.ts +++ b/scripts/jsb.runtime/src/godot.annotations.ts @@ -516,69 +516,6 @@ export function icon(path: string) { /** @deprecated Use createClassBinder() instead. */ export const Icon = icon; -/** @deprecated Use createClassBinder() instead. */ -export function deprecated(message?: string) { - return function (target: any, name?: string) { - legacy_decorators_check(name); - - if (typeof name === "undefined") { - jsb.internal.set_script_doc(target, undefined, 0, message ?? ""); - return; - } - - if (typeof name !== "string" || !name) { - throw new Error("Only methods/properties with a string name/key can be marked as deprecated"); - } - - jsb.internal.set_script_doc(target, name, 0, message ?? ""); - }; -} - -/** @deprecated Use createClassBinder() instead. */ -export const Deprecated = deprecated; - -/** @deprecated Use createClassBinder() instead. */ -export function experimental(message?: string) { - return function (target: any, name?: string) { - legacy_decorators_check(name); - - if (typeof name === "undefined") { - jsb.internal.set_script_doc(target, undefined, 1, message ?? ""); - return; - } - - if (typeof name !== "string" || !name) { - throw new Error("Only methods/properties with a string name/key can be marked as experimental"); - } - - jsb.internal.set_script_doc(target, name, 1, message ?? ""); - }; -} - -/** @deprecated Use createClassBinder() instead. */ -export const Experimental = experimental; - -/** @deprecated Use createClassBinder() instead. */ -export function help(message?: string) { - return function (target: any, name?: string) { - legacy_decorators_check(name); - - if (typeof name === "undefined") { - jsb.internal.set_script_doc(target, undefined, 2, message ?? ""); - return; - } - - if (typeof name !== "string" || !name) { - throw new Error("Only methods/properties with a string name/key can be given a help string"); - } - - jsb.internal.set_script_doc(target, name, 2, message ?? ""); - }; -} - -/** @deprecated Use createClassBinder() instead. */ -export const Help = help; - export type ClassMemberDecorator = ( target: ClassMemberDecoratorTarget, @@ -705,18 +642,6 @@ export function createClassBinder(): ClassBinder { if (icon_path) { jsb.internal.add_script_icon(target, icon_path); } - - for (const [name, message] of Object.entries(deprecated_map)) { - jsb.internal.set_script_doc(proto, name, 0, message ?? ""); - } - - for (const [name, message] of Object.entries(experimental_map)) { - jsb.internal.set_script_doc(proto, name, 1, message ?? ""); - } - - for (const [name, message] of Object.entries(help_map)) { - jsb.internal.set_script_doc(proto, name, 2, message ?? ""); - } }; } @@ -1102,84 +1027,6 @@ export function createClassBinder(): ClassBinder { onready_map[name] = evaluator; }; }, - - // class or member decorators - - deprecated(message?: string) { - return function ( - target: GObjectConstructor, - context: ClassDecoratorContext | ClassValueMemberDecoratorContext, - ) { - if (typeof context !== "object") { - throw new Error( - "The createClassBinder() requires modern decorator support. Disable legacy decorators (experimentalDecorators) in your tsconfig.json", - ); - } - - if (context.kind === "class") { - jsb.internal.set_script_doc(target as GObjectConstructor, undefined, 0, message ?? ""); - return; - } - - const name = typeof context === "object" ? context.name : context; - - if (typeof name !== "string") { - throw new Error("Only methods/properties with a string name/key can be marked as deprecated"); - } - - deprecated_map[name] = message ?? ""; - }; - }, - experimental(message?: string) { - return function ( - target: GObjectConstructor, - context: ClassDecoratorContext | ClassValueMemberDecoratorContext, - ) { - if (typeof context !== "object") { - throw new Error( - "The createClassBinder() requires modern decorator support. Disable legacy decorators (experimentalDecorators) in your tsconfig.json", - ); - } - - if (context.kind === "class") { - jsb.internal.set_script_doc(target as GObjectConstructor, undefined, 1, message ?? ""); - return; - } - - const name = typeof context === "object" ? context.name : context; - - if (typeof name !== "string") { - throw new Error("Only methods/properties with a string name/key can be marked as experimental"); - } - - experimental_map[name] = message ?? ""; - }; - }, - help(message?: string) { - return function ( - target: GObjectConstructor, - context: ClassDecoratorContext | ClassValueMemberDecoratorContext, - ) { - if (typeof context !== "object") { - throw new Error( - "The createClassBinder() requires modern decorator support. Disable legacy decorators (experimentalDecorators) in your tsconfig.json", - ); - } - - if (context.kind === "class") { - jsb.internal.set_script_doc(target as GObjectConstructor, undefined, 2, message ?? ""); - return; - } - - const name = typeof context === "object" ? context.name : context; - - if (typeof name !== "string") { - throw new Error("Only methods/properties with a string name/key can be marked as help"); - } - - help_map[name] = message ?? ""; - }; - }, }), ); } diff --git a/scripts/jsb.runtime/src/jsb.core.ts b/scripts/jsb.runtime/src/jsb.core.ts index 3bfb91ff..ebf75dd7 100644 --- a/scripts/jsb.runtime/src/jsb.core.ts +++ b/scripts/jsb.runtime/src/jsb.core.ts @@ -297,54 +297,6 @@ exports.icon = function (path: string) { }; }; -/** - * FOR BACKWARD COMPATIBILITY ONLY - * @deprecated [WARNING] This function is deprecated. Use the same function from `godot.annotations` instead. - */ -exports.deprecated = function (message?: string) { - return function (target: any, propertyKey?: PropertyKey) { - if (typeof propertyKey === "undefined") { - jsb.internal.set_script_doc(target, undefined, 0, message ?? ""); - return; - } - if (typeof propertyKey !== "string" || propertyKey.length == 0) - throw new Error("only string key is allowed for doc"); - jsb.internal.set_script_doc(target, propertyKey, 0, message ?? ""); - }; -}; - -/** - * FOR BACKWARD COMPATIBILITY ONLY - * @deprecated [WARNING] This function is deprecated. Use the same function from `godot.annotations` instead. - */ -exports.experimental = function (message?: string) { - return function (target: any, propertyKey?: PropertyKey) { - if (typeof propertyKey === "undefined") { - jsb.internal.set_script_doc(target, undefined, 1, message ?? ""); - return; - } - if (typeof propertyKey !== "string" || propertyKey.length == 0) - throw new Error("only string key is allowed for doc"); - jsb.internal.set_script_doc(target, propertyKey, 1, message ?? ""); - }; -}; - -/** - * FOR BACKWARD COMPATIBILITY ONLY - * @deprecated [WARNING] This function is deprecated. Use the same function from `godot.annotations` instead. - */ -exports.help = function (message?: string) { - return function (target: any, propertyKey?: PropertyKey) { - if (typeof propertyKey === "undefined") { - jsb.internal.set_script_doc(target, undefined, 2, message ?? ""); - return; - } - if (typeof propertyKey !== "string" || propertyKey.length == 0) - throw new Error("only string key is allowed for doc"); - jsb.internal.set_script_doc(target, propertyKey, 2, message ?? ""); - }; -}; - /** * FOR BACKWARD COMPATIBILITY ONLY * @deprecated [WARNING] This function is deprecated. Use the same function from `godot` instead. diff --git a/scripts/typings/godot.generated.d.ts b/scripts/typings/godot.generated.d.ts index dde2ce37..a1d13d4e 100644 --- a/scripts/typings/godot.generated.d.ts +++ b/scripts/typings/godot.generated.d.ts @@ -1225,15 +1225,6 @@ declare module "godot.annotations" { onready: ( evaluator: string | GodotJsb.internal.OnReadyEvaluatorFunc, ) => (_target: undefined, context: string | ClassFieldDecoratorContext) => void; - deprecated: ( - message?: string, - ) => (target: GObjectConstructor, context: ClassDecoratorContext | ClassValueMemberDecoratorContext) => void; - experimental: ( - message?: string, - ) => (target: GObjectConstructor, context: ClassDecoratorContext | ClassValueMemberDecoratorContext) => void; - help: ( - message?: string, - ) => (target: GObjectConstructor, context: ClassDecoratorContext | ClassValueMemberDecoratorContext) => void; }; type ExportOptions = { diff --git a/scripts/typings/godot.minimal.d.ts b/scripts/typings/godot.minimal.d.ts index d26ac10a..d540b29e 100644 --- a/scripts/typings/godot.minimal.d.ts +++ b/scripts/typings/godot.minimal.d.ts @@ -131,15 +131,6 @@ declare module "godot-jsb" { function create_script_signal_getter(name: string): (this: GObject) => Signal; function create_script_cached_property_updater(name: string): (this: GObject, value?: unknown) => void; - // 0: deprecated, 1: experimental, 2: help - function set_script_doc( - target: GObjectConstructor, - property_key: undefined, - field: 0 | 1 | 2, - message: string, - ): void; - function set_script_doc(target: GObject, property_key: string, field: 0 | 1 | 2, message: string): void; - function add_module(id: string, obj: any): void; function find_module(id: string): any; function notify_microtasks_run(): void; diff --git a/tests/project/gen/godot/tests/documentation-comments/DocumentationComments.nodes.gen.ts b/tests/project/gen/godot/tests/documentation-comments/DocumentationComments.nodes.gen.ts new file mode 100644 index 00000000..0a0184d7 --- /dev/null +++ b/tests/project/gen/godot/tests/documentation-comments/DocumentationComments.nodes.gen.ts @@ -0,0 +1,5 @@ +declare module "godot" { + interface SceneNodes { + "tests/documentation-comments/DocumentationComments.tscn": { Label: Label<{}>; }; + } +} diff --git a/tests/project/gen/godot/tests/documentation-comments/DocumentationComments.tscn.gen.ts b/tests/project/gen/godot/tests/documentation-comments/DocumentationComments.tscn.gen.ts new file mode 100644 index 00000000..54b3352e --- /dev/null +++ b/tests/project/gen/godot/tests/documentation-comments/DocumentationComments.tscn.gen.ts @@ -0,0 +1,6 @@ +import DocumentationComments from "../../../../tests/documentation-comments/documentation-comments"; +declare module "godot" { + interface ResourceTypes { + "res://tests/documentation-comments/DocumentationComments.tscn": PackedScene; + } +} diff --git a/tests/project/tests/documentation-comments/DocumentationComments.tscn b/tests/project/tests/documentation-comments/DocumentationComments.tscn new file mode 100644 index 00000000..63060302 --- /dev/null +++ b/tests/project/tests/documentation-comments/DocumentationComments.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3 uid="uid://b33p5actrbp6s"] + +[ext_resource type="Script" uid="uid://b13817mvpqwo5" path="res://tests/documentation-comments/documentation-comments.ts" id="1_j3med"] + +[node name="DocumentationComments" type="Node2D"] +script = ExtResource("1_j3med") + +[node name="Label" type="Label" parent="."] +offset_right = 40.0 +offset_bottom = 23.0 +text = "DocumentationComments" diff --git a/tests/project/tests/documentation-comments/documentation-comments.ts b/tests/project/tests/documentation-comments/documentation-comments.ts new file mode 100644 index 00000000..7d3d1272 --- /dev/null +++ b/tests/project/tests/documentation-comments/documentation-comments.ts @@ -0,0 +1,74 @@ +import { Node, Signal, Variant } from "godot"; +import { createClassBinder } from "godot.annotations"; + +const bind = createClassBinder(); + +/** This is a description of the below enum. */ +enum Direction { + /** Direction up. */ + UP = 0, + /** Direction down. */ + DOWN = 1, + /** Direction left. */ + LEFT = 2, + /** Direction right. */ + RIGHT = 3, +} + +/** + * A brief description of the class's role and functionality. + * + * The description of the script, what it can do, + * and any further detail. + * + * @tutorial https://example.com/tutorial_1 + * @tutorial(Tutorial 2) https://example.com/tutorial_2 + * @experimental + */ +@bind() +export default class DocumentationComments extends Node { + /** The description of a signal. */ + @bind.signal() + accessor my_signal!: Signal<() => void>; + + /** The description of a constant. */ + static readonly GRAVITY = 9.8; + + /** The description of the variable v1. */ + v1: any; + + /** + * This is a multiline description of the variable v2. + * The type information below will be extracted for the documentation. + */ + v2: number = 0; + + /** + * If the member has any annotation, the annotation should + * immediately precede it. + */ + @bind.export(Variant.Type.TYPE_INT) + accessor v3: number = some_func(); + + /** + * As the following function is documented, even though its name starts with + * an underscore, it will appear in the help window. + */ + _fn(p1: number, p2: string): number { + return 0; + } + + // The below function isn't documented and its name starts with an underscore + // so it will treated as private and will not be shown in the help window. + _internal(): void {} + + /** + * This function is deprecated and should not appear in the help window. + * @deprecated Use [method _fn] instead + */ + _deprecated() {} +} + +function some_func(): number { + return 0; +} diff --git a/weaver/jsb_script.cpp b/weaver/jsb_script.cpp index 5b3cd3c7..7a1a635a 100644 --- a/weaver/jsb_script.cpp +++ b/weaver/jsb_script.cpp @@ -246,10 +246,458 @@ Error GodotJSScript::reload(bool p_keep_state) } #ifdef TOOLS_ENABLED + +static Dictionary _parse_jsdoc_comment(const String& p_comment, bool p_is_class = false) { + Dictionary result; + String brief_description; + String description; + Array tutorials; + bool is_deprecated = false; + bool is_experimental = false; + String deprecated_msg; + String experimental_msg; + bool in_description = false; + + PackedStringArray lines = p_comment.split("\n"); + for (int i = 0; i < lines.size(); i++) { + String line = lines[i].strip_edges(); + if (line.begins_with("*")) line = line.substr(1).strip_edges(); + + if (line.begins_with("@tutorial")) { + Dictionary tutorial; + String tutorial_line = line.substr(9).strip_edges(); + + // Format: @tutorial(Title): URL or @tutorial(Title) or @tutorial URL + if (tutorial_line.begins_with("(")) { + int close_paren = tutorial_line.find(")"); + if (close_paren > 1) { + tutorial["title"] = tutorial_line.substr(1, close_paren - 1).strip_edges(); + int colon_pos = tutorial_line.find(":", close_paren); + if (colon_pos > close_paren) { + tutorial["link"] = tutorial_line.substr(colon_pos + 1).strip_edges(); + } else { + tutorial["link"] = ""; + } + } else { + tutorial["link"] = tutorial_line; + } + } else { + // Just URL without title + tutorial["link"] = tutorial_line; + } + tutorials.push_back(tutorial); + } else if (line.begins_with("@deprecated")) { + is_deprecated = true; + deprecated_msg = line.substr(11).strip_edges(); + } else if (line.begins_with("@experimental")) { + is_experimental = true; + experimental_msg = line.substr(13).strip_edges(); + } else if (!line.begins_with("@")) { + if (p_is_class) { + if (line.is_empty()) { + if (!brief_description.is_empty() && !in_description) { + in_description = true; + } + } else { + if (!in_description && brief_description.is_empty()) { + brief_description = line; + } else if (!in_description) { + brief_description += " " + line; + } else { + if (!description.is_empty()) description += " "; + description += line; + } + } + } else { + // For members, all text goes into description + if (!line.is_empty()) { + if (!description.is_empty()) description += " "; + description += line; + } + } + } + } + + if (p_is_class && !brief_description.is_empty()) result["brief_description"] = brief_description; + if (!description.is_empty()) result["description"] = description; + if (!tutorials.is_empty()) result["tutorials"] = tutorials; + if (is_deprecated) { + result["deprecated"] = deprecated_msg; + } + if (is_experimental) { + result["experimental"] = experimental_msg; + } + return result; +} + +static Dictionary _extract_class_doc(const String& p_source, const String& p_class_name) { + Dictionary class_dict; + // Try to find "export default class ClassName" first, then fall back to "class ClassName" + int class_pos = p_source.find("export default class " + p_class_name); + if (class_pos == -1) { + class_pos = p_source.find("class " + p_class_name); + } + if (class_pos == -1) return class_dict; + + // Skip backwards past decorators and whitespace to find the comment + int search_pos = class_pos - 1; + while (search_pos > 0) { + // Skip whitespace + while (search_pos > 0 && (p_source[search_pos] == ' ' || p_source[search_pos] == '\t' || p_source[search_pos] == '\n' || p_source[search_pos] == '\r')) { + search_pos--; + } + + // Check if we found the end of a comment + if (search_pos > 0 && p_source[search_pos] == '/' && search_pos > 0 && p_source[search_pos - 1] == '*') { + int comment_end = search_pos; + int comment_start = comment_end - 2; + while (comment_start > 0 && !(p_source[comment_start] == '/' && p_source[comment_start + 1] == '*')) { + comment_start--; + } + if (comment_start >= 0) { + String comment = p_source.substr(comment_start + 2, comment_end - comment_start - 3); + class_dict = _parse_jsdoc_comment(comment, true); + } + break; + } + + // Check if this is a decorator line (starts with @) + int line_start = search_pos; + while (line_start > 0 && p_source[line_start - 1] != '\n' && p_source[line_start - 1] != '\r') { + line_start--; + } + String line = p_source.substr(line_start, search_pos - line_start + 1).strip_edges(); + if (line.begins_with("@")) { + // Skip this decorator line + search_pos = line_start - 1; + } else { + // Not a decorator or comment, stop searching + break; + } + } + return class_dict; +} + +static bool _try_extract_signal(const String& p_declaration, Dictionary& p_doc, Dictionary& r_signals) { + RegEx signal_regex; + signal_regex.compile("@\\w+\\.signal\\(\\)"); + if (!signal_regex.search(p_declaration).is_valid()) return false; + + int name_start = p_declaration.find("accessor"); + if (name_start == -1) return false; + + name_start += 8; + while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { + name_start++; + } + int name_end = name_start; + while (name_end < p_declaration.length() && p_declaration[name_end] != '!' && p_declaration[name_end] != ':' && p_declaration[name_end] != ' ') { + name_end++; + } + if (name_end > name_start) { + String signal_name = p_declaration.substr(name_start, name_end - name_start); + p_doc["name"] = signal_name; + r_signals[signal_name] = p_doc; + return true; + } + return false; +} + +static bool _try_extract_constant(const String& p_declaration, Dictionary& p_doc, Dictionary& r_constants) { + if (p_declaration.find("=") == -1 || p_declaration.find("{") != -1) return false; + + int name_start = 0; + if (p_declaration.begins_with("static readonly")) { + name_start = p_declaration.find("readonly") + 8; + } else if (p_declaration.begins_with("readonly")) { + name_start = 8; + } + + while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { + name_start++; + } + int name_end = name_start; + while (name_end < p_declaration.length() && p_declaration[name_end] != '=' && p_declaration[name_end] != ' ' && p_declaration[name_end] != ':') { + name_end++; + } + if (name_end > name_start) { + String const_name = p_declaration.substr(name_start, name_end - name_start); + + if (const_name.is_empty() || !(const_name[0] >= 'A' && const_name[0] <= 'Z')) return false; + + if (r_constants.has(const_name)) return false; + + p_doc["name"] = const_name; + + int value_start = p_declaration.find("=", name_end); + if (value_start != -1) { + value_start++; + while (value_start < p_declaration.length() && (p_declaration[value_start] == ' ' || p_declaration[value_start] == '\t')) { + value_start++; + } + int value_end = value_start; + while (value_end < p_declaration.length() && p_declaration[value_end] != ';' && p_declaration[value_end] != '\n') { + value_end++; + } + if (value_end > value_start) { + p_doc["value"] = p_declaration.substr(value_start, value_end - value_start).strip_edges(); + } + } + + r_constants[const_name] = p_doc; + return true; + } + return false; +} + +static void _extract_member_docs(const String& p_source, Dictionary& r_properties, Dictionary& r_methods, Dictionary& r_signals, Dictionary& r_constants, Dictionary& r_enums); + +static bool _try_extract_enum(const String& p_declaration, const String& p_source, int p_pos, Dictionary& p_doc, Dictionary& r_enums, Dictionary& r_constants) { + if (!p_declaration.begins_with("enum ")) return false; + + int name_start = 5; + while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { + name_start++; + } + int name_end = name_start; + while (name_end < p_declaration.length() && p_declaration[name_end] != ' ' && p_declaration[name_end] != '{') { + name_end++; + } + if (name_end > name_start) { + String enum_name = p_declaration.substr(name_start, name_end - name_start); + r_enums[enum_name] = p_doc; + + int body_start = p_source.find("{", p_pos); + if (body_start != -1) { + int body_end = p_source.find("}", body_start); + if (body_end != -1) { + String enum_body = p_source.substr(body_start + 1, body_end - body_start - 1); + Dictionary enum_props, enum_methods, enum_signals, enum_constants, enum_enums; + _extract_member_docs(enum_body, enum_props, enum_methods, enum_signals, enum_constants, enum_enums); + + for (const KeyValue& kv : enum_constants) { + Dictionary const_doc = kv.value; + const_doc["enumeration"] = enum_name; + r_constants[kv.key] = const_doc; + } + } + } + return true; + } + return false; +} + +static bool _try_extract_property(const String& p_declaration, Dictionary& p_doc, Dictionary& r_properties) { + if (!(p_declaration.find("accessor") != -1 || (p_declaration.find(":") != -1 && p_declaration.find("(") == -1 && !p_declaration.begins_with("@")))) return false; + + int name_start = p_declaration.find("accessor") != -1 ? p_declaration.find("accessor") + 8 : 0; + while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { + name_start++; + } + int name_end = name_start; + while (name_end < p_declaration.length() && p_declaration[name_end] != ':' && p_declaration[name_end] != '!' && p_declaration[name_end] != '=' && p_declaration[name_end] != ' ') { + name_end++; + } + if (name_end > name_start) { + String prop_name = p_declaration.substr(name_start, name_end - name_start); + + if (r_properties.has(prop_name)) return false; + + p_doc["name"] = prop_name; + + int type_start = p_declaration.find(":", name_end); + if (type_start != -1) { + type_start++; + while (type_start < p_declaration.length() && (p_declaration[type_start] == ' ' || p_declaration[type_start] == '\t')) { + type_start++; + } + int type_end = type_start; + while (type_end < p_declaration.length() && p_declaration[type_end] != '=' && p_declaration[type_end] != ';' && p_declaration[type_end] != '\n') { + type_end++; + } + if (type_end > type_start) { + p_doc["type"] = p_declaration.substr(type_start, type_end - type_start).strip_edges(); + } + } + + // Parse default value after = + int value_start = p_declaration.find("=", name_end); + if (value_start != -1) { + value_start++; + while (value_start < p_declaration.length() && (p_declaration[value_start] == ' ' || p_declaration[value_start] == '\t')) { + value_start++; + } + int value_end = value_start; + while (value_end < p_declaration.length() && p_declaration[value_end] != ';' && p_declaration[value_end] != '\n') { + value_end++; + } + if (value_end > value_start) { + p_doc["default_value"] = p_declaration.substr(value_start, value_end - value_start).strip_edges(); + } + } + + r_properties[prop_name] = p_doc; + return true; + } + return false; +} + +static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, Dictionary& r_methods) { + if (p_declaration.find("(") == -1 || p_declaration.begins_with("@")) return false; + + Dictionary method_doc = p_doc; + String qualifiers; + String return_type; + + if (p_declaration.find("static ") != -1) { + qualifiers = "static"; + } + if (p_declaration.find("async ") != -1) { + if (!qualifiers.is_empty()) qualifiers += " "; + qualifiers += "async"; + } + + int return_start = p_declaration.find("):"); + if (return_start == -1) { + return_start = p_declaration.find(") =>"); + if (return_start != -1) return_start += 5; + } else { + return_start += 3; + } + if (return_start != -1) { + int return_end = return_start; + while (return_end < p_declaration.length() && p_declaration[return_end] != ' ' && p_declaration[return_end] != '{' && p_declaration[return_end] != ';') { + return_end++; + } + if (return_end > return_start) { + return_type = p_declaration.substr(return_start, return_end - return_start).strip_edges(); + } + } + + int name_end = p_declaration.find("("); + int name_start = name_end - 1; + while (name_start >= 0 && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { + name_start--; + } + int name_begin = name_start; + while (name_begin >= 0 && p_declaration[name_begin] != ' ' && p_declaration[name_begin] != '\t' && p_declaration[name_begin] != '\n') { + name_begin--; + } + name_begin++; + if (name_begin <= name_start) { + String method_name = p_declaration.substr(name_begin, name_start - name_begin + 1); + method_doc["name"] = method_name; + if (!qualifiers.is_empty()) { + method_doc["qualifiers"] = qualifiers; + } + if (!return_type.is_empty()) { + method_doc["return_type"] = return_type; + } else { + method_doc["return_type"] = "void"; + } + + int args_start = p_declaration.find("(") + 1; + int args_end = p_declaration.find(")"); + if (args_end > args_start) { + String args_str = p_declaration.substr(args_start, args_end - args_start).strip_edges(); + if (!args_str.is_empty()) { + Array arguments; + PackedStringArray params = args_str.split(","); + for (int i = 0; i < params.size(); i++) { + String param = params[i].strip_edges(); + if (param.is_empty()) continue; + + Dictionary arg_doc; + String arg_name; + String arg_type; + String default_value; + + int eq_pos = param.find("="); + if (eq_pos != -1) { + default_value = param.substr(eq_pos + 1).strip_edges(); + param = param.substr(0, eq_pos).strip_edges(); + } + + int colon_pos = param.find(":"); + if (colon_pos != -1) { + arg_name = param.substr(0, colon_pos).strip_edges(); + arg_type = param.substr(colon_pos + 1).strip_edges(); + } else { + arg_name = param; + arg_type = "any"; + } + + if (arg_name.ends_with("?")) { + arg_name = arg_name.substr(0, arg_name.length() - 1); + } + + arg_doc["name"] = arg_name; + arg_doc["type"] = arg_type; + if (!default_value.is_empty()) { + arg_doc["default_value"] = default_value; + } + arguments.push_back(arg_doc); + } + if (!arguments.is_empty()) { + method_doc["arguments"] = arguments; + } + } + } + + r_methods[method_name] = method_doc; + return true; + } + return false; +} + +static void _extract_member_docs(const String& p_source, Dictionary& r_properties, Dictionary& r_methods, Dictionary& r_signals, Dictionary& r_constants, Dictionary& r_enums) { + int pos = 0; + while (pos < p_source.length()) { + int comment_start = p_source.find("/**", pos); + if (comment_start == -1) break; + int comment_end = p_source.find("*/", comment_start); + if (comment_end == -1) break; + + String comment = p_source.substr(comment_start + 3, comment_end - comment_start - 3); + Dictionary doc = _parse_jsdoc_comment(comment, false); + + int next_pos = comment_end + 2; + while (next_pos < p_source.length() && (p_source[next_pos] == ' ' || p_source[next_pos] == '\t' || p_source[next_pos] == '\n' || p_source[next_pos] == '\r')) { + next_pos++; + } + + // Get multiple lines to handle decorators on separate lines + String remaining = p_source.substr(next_pos, 400); + PackedStringArray lines = remaining.split("\n"); + + // Check first line for decorator + String first_line = lines.size() > 0 ? lines[0].strip_edges() : ""; + String declaration; + + // If first line is a decorator, check the next line for the actual declaration + if (first_line.begins_with("@")) { + declaration = first_line; + if (lines.size() > 1) { + declaration += " " + lines[1].strip_edges(); + } + } else { + declaration = first_line; + } + + if (_try_extract_signal(declaration, doc, r_signals)) { + } else if (_try_extract_constant(declaration, doc, r_constants)) { + } else if (_try_extract_enum(declaration, p_source, next_pos, doc, r_enums, r_constants)) { + } else if (_try_extract_property(declaration, doc, r_properties)) { + } else if (_try_extract_method(declaration, doc, r_methods)) { + } + + pos = comment_end + 2; + } +} + #if GODOT_4_4_OR_NEWER StringName GodotJSScript::get_doc_class_name() const { - //TODO not verified Vector docs = get_documentation(); if (!docs.is_empty()) return docs[0].name; return {}; @@ -263,33 +711,77 @@ Vector GodotJSScript::get_documentation() const String base_type; const String class_name = GodotJSScriptLanguage::get_singleton()->get_global_class_name(get_path(), &base_type); - DocData::ClassDoc class_doc_data; - - class_doc_data.name = class_name; - class_doc_data.inherits = base_type.is_empty() ? "Object" : base_type; - class_doc_data.is_script_doc = true; - class_doc_data.brief_description = script_class_info_.doc.brief_description; - class_doc_data.is_deprecated = script_class_info_.doc.is_deprecated; - class_doc_data.is_experimental = script_class_info_.doc.is_experimental; -#if GODOT_4_3_OR_NEWER - class_doc_data.deprecated_message = script_class_info_.doc.deprecated_message; - class_doc_data.experimental_message = script_class_info_.doc.experimental_message; -#endif - class_doc_data.script_path = get_path(); - for (const auto& item : script_class_info_.properties) - { - DocData::PropertyDoc property_doc_data; - property_doc_data.name = item.key; - property_doc_data.description = item.value.doc.brief_description; - property_doc_data.is_deprecated = item.value.doc.is_deprecated; - property_doc_data.is_experimental = item.value.doc.is_experimental; -#if GODOT_4_3_OR_NEWER - property_doc_data.deprecated_message = item.value.doc.deprecated_message; - property_doc_data.experimental_message = item.value.doc.experimental_message; -#endif - class_doc_data.properties.append(property_doc_data); + + Dictionary class_dict; + class_dict["name"] = class_name; + class_dict["inherits"] = base_type.is_empty() ? "Object" : base_type; + class_dict["is_script_doc"] = true; + class_dict["script_path"] = get_path(); + + const String source = get_source_code(); + if (!source.is_empty()) { + Dictionary class_doc = _extract_class_doc(source, class_name); + if (class_doc.has("brief_description")) { + class_dict["brief_description"] = class_doc["brief_description"]; + } + if (class_doc.has("description")) { + class_dict["description"] = class_doc["description"]; + } + if (class_doc.has("tutorials")) { + class_dict["tutorials"] = class_doc["tutorials"]; + } + if (class_doc.has("deprecated")) { + class_dict["deprecated"] = class_doc["deprecated"]; + } + if (class_doc.has("experimental")) { + class_dict["experimental"] = class_doc["experimental"]; + } + + Dictionary properties; + Dictionary methods; + Dictionary signals; + Dictionary constants; + Dictionary enums; + _extract_member_docs(source, properties, methods, signals, constants, enums); + + Array properties_array; + for (const KeyValue& kv : properties) { + properties_array.push_back(kv.value); + } + if (!properties_array.is_empty()) { + class_dict["properties"] = properties_array; + } + + Array methods_array; + for (const KeyValue& kv : methods) { + methods_array.push_back(kv.value); + } + if (!methods_array.is_empty()) { + class_dict["methods"] = methods_array; + } + + Array signals_array; + for (const KeyValue& kv : signals) { + signals_array.push_back(kv.value); + } + if (!signals_array.is_empty()) { + class_dict["signals"] = signals_array; + } + + Array constants_array; + for (const KeyValue& kv : constants) { + constants_array.push_back(kv.value); + } + if (!constants_array.is_empty()) { + class_dict["constants"] = constants_array; + } + + if (!enums.is_empty()) { + class_dict["enums"] = enums; + } } + DocData::ClassDoc class_doc_data = DocData::ClassDoc::from_dict(class_dict); return { class_doc_data }; } From a8b56be06c001bbcb42dfdd2141aebb41775ad9d Mon Sep 17 00:00:00 2001 From: Nicolas Merget Date: Sat, 20 Dec 2025 01:04:12 +0100 Subject: [PATCH 2/3] fix: range based error --- weaver/jsb_script.cpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/weaver/jsb_script.cpp b/weaver/jsb_script.cpp index 7a1a635a..307fcf59 100644 --- a/weaver/jsb_script.cpp +++ b/weaver/jsb_script.cpp @@ -475,10 +475,12 @@ static bool _try_extract_enum(const String& p_declaration, const String& p_sourc Dictionary enum_props, enum_methods, enum_signals, enum_constants, enum_enums; _extract_member_docs(enum_body, enum_props, enum_methods, enum_signals, enum_constants, enum_enums); - for (const KeyValue& kv : enum_constants) { - Dictionary const_doc = kv.value; + Array keys = enum_constants.keys(); + for (int i = 0; i < keys.size(); i++) { + Variant key = keys[i]; + Dictionary const_doc = enum_constants[key]; const_doc["enumeration"] = enum_name; - r_constants[kv.key] = const_doc; + r_constants[key] = const_doc; } } } @@ -745,32 +747,36 @@ Vector GodotJSScript::get_documentation() const _extract_member_docs(source, properties, methods, signals, constants, enums); Array properties_array; - for (const KeyValue& kv : properties) { - properties_array.push_back(kv.value); + Array keys = properties.keys(); + for (int i = 0; i < keys.size(); i++) { + properties_array.push_back(properties[keys[i]]); } if (!properties_array.is_empty()) { class_dict["properties"] = properties_array; } Array methods_array; - for (const KeyValue& kv : methods) { - methods_array.push_back(kv.value); + keys = methods.keys(); + for (int i = 0; i < keys.size(); i++) { + methods_array.push_back(methods[keys[i]]); } if (!methods_array.is_empty()) { class_dict["methods"] = methods_array; } Array signals_array; - for (const KeyValue& kv : signals) { - signals_array.push_back(kv.value); + keys = signals.keys(); + for (int i = 0; i < keys.size(); i++) { + signals_array.push_back(signals[keys[i]]); } if (!signals_array.is_empty()) { class_dict["signals"] = signals_array; } Array constants_array; - for (const KeyValue& kv : constants) { - constants_array.push_back(kv.value); + keys = constants.keys(); + for (int i = 0; i < keys.size(); i++) { + constants_array.push_back(constants[keys[i]]); } if (!constants_array.is_empty()) { class_dict["constants"] = constants_array; From 9c005ac80136fb31055a625257afce7711bdd9bb Mon Sep 17 00:00:00 2001 From: Nicolas Merget Date: Sun, 28 Dec 2025 20:59:44 +0100 Subject: [PATCH 3/3] feat: add a setting for jsdoc documentation comments --- .changeset/silent-clouds-matter.md | 8 +- bridge/jsb_bridge_module_loader.cpp | 78 ++++++ bridge/jsb_class_info.cpp | 73 +++++- bridge/jsb_class_info.h | 29 +++ internal/jsb_settings.cpp | 10 +- internal/jsb_settings.h | 1 + scripts/jsb.editor/src/jsb.editor.codegen.ts | 83 ++++++- scripts/jsb.runtime/src/godot.annotations.ts | 153 ++++++++++++ scripts/jsb.runtime/src/jsb.core.ts | 48 ++++ scripts/typings/godot.generated.d.ts | 9 + scripts/typings/godot.minimal.d.ts | 9 + weaver/jsb_script.cpp | 244 +++++++++++-------- 12 files changed, 632 insertions(+), 113 deletions(-) diff --git a/.changeset/silent-clouds-matter.md b/.changeset/silent-clouds-matter.md index 64ed8bc8..e07ddb5c 100644 --- a/.changeset/silent-clouds-matter.md +++ b/.changeset/silent-clouds-matter.md @@ -1,10 +1,12 @@ --- -"@godot-js/editor": major +"@godot-js/editor": minor --- -feat: replaced @bind.help/experimental/deprecated with standard JS comments and JSDoc like annotations +feat: additionally to @bind.help/experimental/deprecated add an editor setting to enable standard JS comments and JSDoc like annotations for documentation comments. -Here is an example for more information checkout https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_documentation_comments.html: +You need to enable the setting, set ``Editor -> Editor Settings -> GodotJS -> Experimental -> Jsdoc Documentation Comments `` to ``on``. + +Here is an example for more information see https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_documentation_comments.html: ````ts /** * A brief description of the class's role and functionality. diff --git a/bridge/jsb_bridge_module_loader.cpp b/bridge/jsb_bridge_module_loader.cpp index 65619d12..0c17333e 100644 --- a/bridge/jsb_bridge_module_loader.cpp +++ b/bridge/jsb_bridge_module_loader.cpp @@ -272,6 +272,83 @@ namespace jsb impl::Helper::to_string_opt(isolate, evaluator->Get(context, jsb_name(environment, name)))); } + // function set_script_doc(target: GObjectConstructor, property_key: undefined, field: 0 | 1 | 2, message: string): void; + // function set_script_doc(target: GObject, property_key: string, field: 0 | 1 | 2, message: string): void; + void _set_script_doc(const v8::FunctionCallbackInfo& info) + { +#ifdef TOOLS_ENABLED + constexpr int kTarget = 0; // constructor | prototype + constexpr int kProperty = 1; // undefined | string + constexpr int kField = 2; // int32: 0, 1, 2 + constexpr int kMessage = 3; // undefined | string + + v8::Isolate* isolate = info.GetIsolate(); + v8::HandleScope handle_scope(isolate); + const v8::Local context = isolate->GetCurrentContext(); + if (info.Length() != 4 || !info[kTarget]->IsObject() || !info[kField]->IsNumber()) + { + jsb_throw(isolate, "bad param"); + return; + } + Environment* environment = Environment::wrap(isolate); + const v8::Local target = info[kTarget].As(); + const v8::Local property = info[kProperty].As(); + const ScriptClassDocField::Type doc_item = (ScriptClassDocField::Type) info[kField].As()->Value(); + const v8::Local message = info[kMessage]->IsString() ? info[kMessage].As() : v8::String::Empty(isolate); + v8::Local doc; + + // always set doc info on `prototype` + if (property->IsUndefined()) + { + // doc for class + const v8::Local prototype = target->Get(context, jsb_name(environment, prototype)).ToLocalChecked().As(); + jsb_check(prototype->IsObject()); + if (v8::Local val; !prototype->Get(context, jsb_symbol(environment, Doc)).ToLocal(&val) || !val->IsObject()) + { + doc = v8::Object::New(isolate); + prototype->Set(context, jsb_symbol(environment, Doc), doc).Check(); + } + else + { + doc = val.As(); + } + } + else + { + // doc for member + jsb_check(property->IsString() && property.As()->Length() != 0); + v8::Local member_doc_map; + if (v8::Local val; !target->Get(context, jsb_symbol(environment, MemberDocMap)).ToLocal(&val) || !val->IsMap()) + { + member_doc_map = v8::Map::New(isolate); + target->Set(context, jsb_symbol(environment, MemberDocMap), member_doc_map).Check(); + } + else + { + member_doc_map = val.As(); + } + + if (v8::Local val; !member_doc_map->Get(context, property).ToLocal(&val) || !val->IsObject()) + { + doc = v8::Object::New(isolate); + jsb_ensure(!member_doc_map->Set(context, property, doc).IsEmpty()); + } + else + { + doc = val.As(); + } + } + + switch (doc_item) + { + case ScriptClassDocField::Deprecated: doc->Set(context, jsb_name(environment, deprecated), message).Check(); return; + case ScriptClassDocField::Experimental: doc->Set(context, jsb_name(environment, experimental), message).Check(); return; + case ScriptClassDocField::Help: doc->Set(context, jsb_name(environment, help), message).Check(); return; + } + jsb_throw(isolate, "bad param"); +#endif + } + // function add_script_property(prototype: GObject, details: ScriptPropertyInfo): void void _add_script_property(const v8::FunctionCallbackInfo& info) { @@ -504,6 +581,7 @@ namespace jsb internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "add_script_tool"), JSB_NEW_FUNCTION(context, _add_script_tool, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "add_script_icon"), JSB_NEW_FUNCTION(context, _add_script_icon, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "add_script_rpc"), JSB_NEW_FUNCTION(context, _add_script_rpc, {})).Check(); + internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "set_script_doc"), JSB_NEW_FUNCTION(context, _set_script_doc, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "notify_microtasks_run"), JSB_NEW_FUNCTION(context, _notify_microtasks_run, {})).Check(); internal_obj->Set(context, impl::Helper::new_string_ascii(isolate, "create_script_cached_property_updater"), JSB_NEW_FUNCTION(context, _create_script_cached_property_updater, {})).Check(); diff --git a/bridge/jsb_class_info.cpp b/bridge/jsb_class_info.cpp index 1f34090c..7d0265a2 100644 --- a/bridge/jsb_class_info.cpp +++ b/bridge/jsb_class_info.cpp @@ -7,6 +7,52 @@ namespace jsb { +#ifdef TOOLS_ENABLED + void _parse_script_doc(v8::Isolate* isolate, const v8::Local& context, + const v8::MaybeLocal holder, ScriptBaseDoc& r_doc) + { + if (v8::Local tv; holder.IsEmpty() || !holder.ToLocal(&tv) || !tv->IsObject()) + { + // invalid + } + else + { + const v8::Local obj = tv.As(); + Environment* environment = Environment::wrap(isolate); + + // (@deprecated) + if (v8::Local val; obj->Get(context, jsb_name(environment, deprecated)).ToLocal(&val) && val->IsString()) + { + r_doc.is_deprecated = true; + r_doc.deprecated_message = impl::Helper::to_string(isolate, val); + } + else + { + r_doc.is_deprecated = false; + } + // (@experimental) + if (v8::Local val; obj->Get(context, jsb_name(environment, experimental)).ToLocal(&val) && val->IsString()) + { + r_doc.is_experimental = true; + r_doc.experimental_message = impl::Helper::to_string(isolate, val); + } + else + { + r_doc.is_experimental = false; + } + // (@help) + if (v8::Local val; obj->Get(context, jsb_name(environment, help)).ToLocal(&val) && val->IsString()) + { + r_doc.brief_description = impl::Helper::to_string(isolate, val); + } + else + { + r_doc.brief_description.clear(); + } + } + } +#endif + //NOTE ensure the address of p_class_info being locked during this procedure void _parse_script_class_iterate(const v8::Local& p_context, const ScriptClassInfoPtr& p_class_info, const v8::Local& class_obj) { @@ -34,6 +80,16 @@ namespace jsb JSB_LOG(VeryVerbose, "godot js class name %s (native: %s)", p_class_info->js_class_name, p_class_info->native_class_name); +#ifdef TOOLS_ENABLED + // class doc + v8::Local doc_map; + if (v8::Local val; prototype->Get(p_context, jsb_symbol(environment, MemberDocMap)).ToLocal(&val) && val->IsMap()) + { + doc_map = val.As(); + } + _parse_script_doc(isolate, p_context, prototype->Get(p_context, jsb_symbol(environment, Doc)), p_class_info->doc); +#endif + // class rpc config v8::Local rpc_config_map; if (v8::Local val; prototype->Get(p_context, jsb_symbol(environment, ClassRPCConfig)).ToLocal(&val) && val->IsMap()) @@ -61,8 +117,15 @@ namespace jsb v8::Local prop_val; if (prop_descriptor.As()->Get(p_context, jsb_name(environment, value)).ToLocal(&prop_val) && prop_val->IsFunction()) { - - p_class_info->methods.insert((StringName) name_s, {}); + //TODO property categories + ScriptMethodInfo method_info = {}; +#ifdef TOOLS_ENABLED + if (v8::Local val; !doc_map.IsEmpty() && doc_map->Get(p_context, prop_name).ToLocal(&val) && val->IsObject()) + { + _parse_script_doc(isolate, p_context, val, method_info.doc); + } +#endif // TOOLS_ENABLED + p_class_info->methods.insert((StringName) name_s, method_info); // check rpc config if (v8::Local rpc_val; @@ -168,6 +231,12 @@ namespace jsb property_info.cache = cache->BooleanValue(isolate); } +#ifdef TOOLS_ENABLED + if (v8::Local val; !doc_map.IsEmpty() && doc_map->Get(p_context, prop_name).ToLocal(&val) && val->IsObject()) + { + _parse_script_doc(isolate, p_context, val, property_info.doc); + } +#endif // TOOLS_ENABLED p_class_info->properties.insert(property_info.name, property_info); JSB_LOG(VeryVerbose, "... property %s: %s", property_info.name, Variant::get_type_name(property_info.type)); } diff --git a/bridge/jsb_class_info.h b/bridge/jsb_class_info.h index 0d54b5c4..f5b0b6a6 100644 --- a/bridge/jsb_class_info.h +++ b/bridge/jsb_class_info.h @@ -93,6 +93,27 @@ namespace jsb }; } +#ifdef TOOLS_ENABLED + struct ScriptBaseDoc + { + String brief_description; + + String deprecated_message; + String experimental_message; + + bool is_deprecated = false; + bool is_experimental = false; + + }; + + struct ScriptClassDoc : ScriptBaseDoc {}; + struct ScriptMethodDoc : ScriptBaseDoc {}; + struct ScriptPropertyDoc : ScriptBaseDoc {}; +#else + struct ScriptClassDoc {}; + struct ScriptMethodDoc {}; + struct ScriptPropertyDoc {}; +#endif namespace ScriptMethodFlags { @@ -109,6 +130,9 @@ namespace jsb struct ScriptMethodInfo { + // only valid with TOOLS_ENABLED + ScriptMethodDoc doc; + ScriptMethodFlags::Type flags = ScriptMethodFlags::None; // v8::Global cache_; @@ -130,6 +154,8 @@ namespace jsb String hint_string; + ScriptPropertyDoc doc; + // valid only if _Evaluated flag is set in ScriptClassInfo.flags Variant default_value; @@ -175,6 +201,9 @@ namespace jsb // script icon path for showing in scene hierarchy String icon; + // only valid with TOOLS_ENABLED + ScriptClassDoc doc; + Dictionary rpc_config; HashMap methods; diff --git a/internal/jsb_settings.cpp b/internal/jsb_settings.cpp index 11e64850..385fcc99 100644 --- a/internal/jsb_settings.cpp +++ b/internal/jsb_settings.cpp @@ -12,6 +12,7 @@ namespace jsb::internal { #ifdef TOOLS_ENABLED + static constexpr char kEdExperimentalJSDocDocumentationComments[] = JSB_MODULE_NAME_STRING "/experimental/jsdoc_documentation_comments"; static constexpr char kEdDebuggerPort[] = JSB_MODULE_NAME_STRING "/debugger/editor_port"; static constexpr char kEdIgnoredClasses[] = JSB_MODULE_NAME_STRING "/codegen/ignored_classes"; static constexpr char kEdAutogenPath[] = JSB_MODULE_NAME_STRING "/codegen/autogen_path"; @@ -61,7 +62,8 @@ namespace jsb::internal if (EditorSettings::get_singleton()) { inited = true; - _EDITOR_DEF(kEdDebuggerPort, 9230, true); + _EDITOR_DEF(kEdExperimentalJSDocDocumentationComments, false, true); + _EDITOR_DEF(kEdDebuggerPort, 9230, true); _EDITOR_DEF(kEdIgnoredClasses, PackedStringArray(), false); _EDITOR_DEF(kEdAutogenPath, "gen/godot", false); _EDITOR_DEF(kEdGenSceneDTS, true, false); @@ -174,6 +176,12 @@ namespace jsb::internal init_editor_settings(); return EDITOR_GET(kEdCodegenUseProjectSettings); } + + bool Settings::get_jsdoc_documentation_comments() + { + init_editor_settings(); + return EDITOR_GET(kEdExperimentalJSDocDocumentationComments); + } #endif bool Settings::is_packaging_with_source_map() diff --git a/internal/jsb_settings.h b/internal/jsb_settings.h index bb84f4f6..1ad332d5 100644 --- a/internal/jsb_settings.h +++ b/internal/jsb_settings.h @@ -57,6 +57,7 @@ namespace jsb::internal static bool get_autogen_resource_dts_on_save(); static bool get_gen_resource_dts(); static bool get_codegen_use_project_settings(); + static bool get_jsdoc_documentation_comments(); #endif }; } diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index 24aaef3a..0a5eb934 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -1888,7 +1888,88 @@ const annotation_types = { }, ], }, - } + }, + deprecated: { + type: DescriptorType.FunctionLiteral, + parameters: [ + { name: "message", type: { type: DescriptorType.Godot, name: "string" }, optional: true }, + ], + returns: { + type: DescriptorType.Godot, + name: "Decorator", + arguments: [ + { + type: DescriptorType.Union, + types: [ + { + type: DescriptorType.Godot, + name: "ClassDecoratorContext", + arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], + }, + { + type: DescriptorType.Godot, + name: "ClassValueMemberDecoratorContext", + arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], + }, + ], + }, + ], + }, + }, + experimental: { + type: DescriptorType.FunctionLiteral, + parameters: [ + { name: "message", type: { type: DescriptorType.Godot, name: "string" }, optional: true }, + ], + returns: { + type: DescriptorType.Godot, + name: "Decorator", + arguments: [ + { + type: DescriptorType.Union, + types: [ + { + type: DescriptorType.Godot, + name: "ClassDecoratorContext", + arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], + }, + { + type: DescriptorType.Godot, + name: "ClassValueMemberDecoratorContext", + arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], + }, + ], + }, + ], + }, + }, + help: { + type: DescriptorType.FunctionLiteral, + parameters: [ + { name: "message", type: { type: DescriptorType.Godot, name: "string" }, optional: true }, + ], + returns: { + type: DescriptorType.Godot, + name: "Decorator", + arguments: [ + { + type: DescriptorType.Union, + types: [ + { + type: DescriptorType.Godot, + name: "ClassDecoratorContext", + arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], + }, + { + type: DescriptorType.Godot, + name: "ClassValueMemberDecoratorContext", + arguments: [{ type: DescriptorType.Godot, name: "GObjectConstructor" }], + }, + ], + }, + ], + }, + }, }, }, ], diff --git a/scripts/jsb.runtime/src/godot.annotations.ts b/scripts/jsb.runtime/src/godot.annotations.ts index 235df317..1221f4e2 100644 --- a/scripts/jsb.runtime/src/godot.annotations.ts +++ b/scripts/jsb.runtime/src/godot.annotations.ts @@ -516,6 +516,69 @@ export function icon(path: string) { /** @deprecated Use createClassBinder() instead. */ export const Icon = icon; +/** @deprecated Use createClassBinder() instead. */ +export function deprecated(message?: string) { + return function (target: any, name?: string) { + legacy_decorators_check(name); + + if (typeof name === "undefined") { + jsb.internal.set_script_doc(target, undefined, 0, message ?? ""); + return; + } + + if (typeof name !== "string" || !name) { + throw new Error("Only methods/properties with a string name/key can be marked as deprecated"); + } + + jsb.internal.set_script_doc(target, name, 0, message ?? ""); + }; +} + +/** @deprecated Use createClassBinder() instead. */ +export const Deprecated = deprecated; + +/** @deprecated Use createClassBinder() instead. */ +export function experimental(message?: string) { + return function (target: any, name?: string) { + legacy_decorators_check(name); + + if (typeof name === "undefined") { + jsb.internal.set_script_doc(target, undefined, 1, message ?? ""); + return; + } + + if (typeof name !== "string" || !name) { + throw new Error("Only methods/properties with a string name/key can be marked as experimental"); + } + + jsb.internal.set_script_doc(target, name, 1, message ?? ""); + }; +} + +/** @deprecated Use createClassBinder() instead. */ +export const Experimental = experimental; + +/** @deprecated Use createClassBinder() instead. */ +export function help(message?: string) { + return function (target: any, name?: string) { + legacy_decorators_check(name); + + if (typeof name === "undefined") { + jsb.internal.set_script_doc(target, undefined, 2, message ?? ""); + return; + } + + if (typeof name !== "string" || !name) { + throw new Error("Only methods/properties with a string name/key can be given a help string"); + } + + jsb.internal.set_script_doc(target, name, 2, message ?? ""); + }; +} + +/** @deprecated Use createClassBinder() instead. */ +export const Help = help; + export type ClassMemberDecorator = ( target: ClassMemberDecoratorTarget, @@ -642,6 +705,18 @@ export function createClassBinder(): ClassBinder { if (icon_path) { jsb.internal.add_script_icon(target, icon_path); } + + for (const [name, message] of Object.entries(deprecated_map)) { + jsb.internal.set_script_doc(proto, name, 0, message ?? ""); + } + + for (const [name, message] of Object.entries(experimental_map)) { + jsb.internal.set_script_doc(proto, name, 1, message ?? ""); + } + + for (const [name, message] of Object.entries(help_map)) { + jsb.internal.set_script_doc(proto, name, 2, message ?? ""); + } }; } @@ -1027,6 +1102,84 @@ export function createClassBinder(): ClassBinder { onready_map[name] = evaluator; }; }, + + // class or member decorators + + deprecated(message?: string) { + return function ( + target: GObjectConstructor, + context: ClassDecoratorContext | ClassValueMemberDecoratorContext, + ) { + if (typeof context !== "object") { + throw new Error( + "The createClassBinder() requires modern decorator support. Disable legacy decorators (experimentalDecorators) in your tsconfig.json", + ); + } + + if (context.kind === "class") { + jsb.internal.set_script_doc(target as GObjectConstructor, undefined, 0, message ?? ""); + return; + } + + const name = typeof context === "object" ? context.name : context; + + if (typeof name !== "string") { + throw new Error("Only methods/properties with a string name/key can be marked as deprecated"); + } + + deprecated_map[name] = message ?? ""; + }; + }, + experimental(message?: string) { + return function ( + target: GObjectConstructor, + context: ClassDecoratorContext | ClassValueMemberDecoratorContext, + ) { + if (typeof context !== "object") { + throw new Error( + "The createClassBinder() requires modern decorator support. Disable legacy decorators (experimentalDecorators) in your tsconfig.json", + ); + } + + if (context.kind === "class") { + jsb.internal.set_script_doc(target as GObjectConstructor, undefined, 1, message ?? ""); + return; + } + + const name = typeof context === "object" ? context.name : context; + + if (typeof name !== "string") { + throw new Error("Only methods/properties with a string name/key can be marked as experimental"); + } + + experimental_map[name] = message ?? ""; + }; + }, + help(message?: string) { + return function ( + target: GObjectConstructor, + context: ClassDecoratorContext | ClassValueMemberDecoratorContext, + ) { + if (typeof context !== "object") { + throw new Error( + "The createClassBinder() requires modern decorator support. Disable legacy decorators (experimentalDecorators) in your tsconfig.json", + ); + } + + if (context.kind === "class") { + jsb.internal.set_script_doc(target as GObjectConstructor, undefined, 2, message ?? ""); + return; + } + + const name = typeof context === "object" ? context.name : context; + + if (typeof name !== "string") { + throw new Error("Only methods/properties with a string name/key can be marked as help"); + } + + help_map[name] = message ?? ""; + }; + }, }), ); } diff --git a/scripts/jsb.runtime/src/jsb.core.ts b/scripts/jsb.runtime/src/jsb.core.ts index ebf75dd7..3bfb91ff 100644 --- a/scripts/jsb.runtime/src/jsb.core.ts +++ b/scripts/jsb.runtime/src/jsb.core.ts @@ -297,6 +297,54 @@ exports.icon = function (path: string) { }; }; +/** + * FOR BACKWARD COMPATIBILITY ONLY + * @deprecated [WARNING] This function is deprecated. Use the same function from `godot.annotations` instead. + */ +exports.deprecated = function (message?: string) { + return function (target: any, propertyKey?: PropertyKey) { + if (typeof propertyKey === "undefined") { + jsb.internal.set_script_doc(target, undefined, 0, message ?? ""); + return; + } + if (typeof propertyKey !== "string" || propertyKey.length == 0) + throw new Error("only string key is allowed for doc"); + jsb.internal.set_script_doc(target, propertyKey, 0, message ?? ""); + }; +}; + +/** + * FOR BACKWARD COMPATIBILITY ONLY + * @deprecated [WARNING] This function is deprecated. Use the same function from `godot.annotations` instead. + */ +exports.experimental = function (message?: string) { + return function (target: any, propertyKey?: PropertyKey) { + if (typeof propertyKey === "undefined") { + jsb.internal.set_script_doc(target, undefined, 1, message ?? ""); + return; + } + if (typeof propertyKey !== "string" || propertyKey.length == 0) + throw new Error("only string key is allowed for doc"); + jsb.internal.set_script_doc(target, propertyKey, 1, message ?? ""); + }; +}; + +/** + * FOR BACKWARD COMPATIBILITY ONLY + * @deprecated [WARNING] This function is deprecated. Use the same function from `godot.annotations` instead. + */ +exports.help = function (message?: string) { + return function (target: any, propertyKey?: PropertyKey) { + if (typeof propertyKey === "undefined") { + jsb.internal.set_script_doc(target, undefined, 2, message ?? ""); + return; + } + if (typeof propertyKey !== "string" || propertyKey.length == 0) + throw new Error("only string key is allowed for doc"); + jsb.internal.set_script_doc(target, propertyKey, 2, message ?? ""); + }; +}; + /** * FOR BACKWARD COMPATIBILITY ONLY * @deprecated [WARNING] This function is deprecated. Use the same function from `godot` instead. diff --git a/scripts/typings/godot.generated.d.ts b/scripts/typings/godot.generated.d.ts index a1d13d4e..dde2ce37 100644 --- a/scripts/typings/godot.generated.d.ts +++ b/scripts/typings/godot.generated.d.ts @@ -1225,6 +1225,15 @@ declare module "godot.annotations" { onready: ( evaluator: string | GodotJsb.internal.OnReadyEvaluatorFunc, ) => (_target: undefined, context: string | ClassFieldDecoratorContext) => void; + deprecated: ( + message?: string, + ) => (target: GObjectConstructor, context: ClassDecoratorContext | ClassValueMemberDecoratorContext) => void; + experimental: ( + message?: string, + ) => (target: GObjectConstructor, context: ClassDecoratorContext | ClassValueMemberDecoratorContext) => void; + help: ( + message?: string, + ) => (target: GObjectConstructor, context: ClassDecoratorContext | ClassValueMemberDecoratorContext) => void; }; type ExportOptions = { diff --git a/scripts/typings/godot.minimal.d.ts b/scripts/typings/godot.minimal.d.ts index d540b29e..d26ac10a 100644 --- a/scripts/typings/godot.minimal.d.ts +++ b/scripts/typings/godot.minimal.d.ts @@ -131,6 +131,15 @@ declare module "godot-jsb" { function create_script_signal_getter(name: string): (this: GObject) => Signal; function create_script_cached_property_updater(name: string): (this: GObject, value?: unknown) => void; + // 0: deprecated, 1: experimental, 2: help + function set_script_doc( + target: GObjectConstructor, + property_key: undefined, + field: 0 | 1 | 2, + message: string, + ): void; + function set_script_doc(target: GObject, property_key: string, field: 0 | 1 | 2, message: string): void; + function add_module(id: string, obj: any): void; function find_module(id: string): any; function notify_microtasks_run(): void; diff --git a/weaver/jsb_script.cpp b/weaver/jsb_script.cpp index 307fcf59..296dab78 100644 --- a/weaver/jsb_script.cpp +++ b/weaver/jsb_script.cpp @@ -266,7 +266,7 @@ static Dictionary _parse_jsdoc_comment(const String& p_comment, bool p_is_class if (line.begins_with("@tutorial")) { Dictionary tutorial; String tutorial_line = line.substr(9).strip_edges(); - + // Format: @tutorial(Title): URL or @tutorial(Title) or @tutorial URL if (tutorial_line.begins_with("(")) { int close_paren = tutorial_line.find(")"); @@ -346,7 +346,7 @@ static Dictionary _extract_class_doc(const String& p_source, const String& p_cla while (search_pos > 0 && (p_source[search_pos] == ' ' || p_source[search_pos] == '\t' || p_source[search_pos] == '\n' || p_source[search_pos] == '\r')) { search_pos--; } - + // Check if we found the end of a comment if (search_pos > 0 && p_source[search_pos] == '/' && search_pos > 0 && p_source[search_pos - 1] == '*') { int comment_end = search_pos; @@ -360,7 +360,7 @@ static Dictionary _extract_class_doc(const String& p_source, const String& p_cla } break; } - + // Check if this is a decorator line (starts with @) int line_start = search_pos; while (line_start > 0 && p_source[line_start - 1] != '\n' && p_source[line_start - 1] != '\r') { @@ -382,10 +382,10 @@ static bool _try_extract_signal(const String& p_declaration, Dictionary& p_doc, RegEx signal_regex; signal_regex.compile("@\\w+\\.signal\\(\\)"); if (!signal_regex.search(p_declaration).is_valid()) return false; - + int name_start = p_declaration.find("accessor"); if (name_start == -1) return false; - + name_start += 8; while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { name_start++; @@ -405,14 +405,14 @@ static bool _try_extract_signal(const String& p_declaration, Dictionary& p_doc, static bool _try_extract_constant(const String& p_declaration, Dictionary& p_doc, Dictionary& r_constants) { if (p_declaration.find("=") == -1 || p_declaration.find("{") != -1) return false; - + int name_start = 0; if (p_declaration.begins_with("static readonly")) { name_start = p_declaration.find("readonly") + 8; } else if (p_declaration.begins_with("readonly")) { name_start = 8; } - + while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { name_start++; } @@ -422,13 +422,13 @@ static bool _try_extract_constant(const String& p_declaration, Dictionary& p_doc } if (name_end > name_start) { String const_name = p_declaration.substr(name_start, name_end - name_start); - + if (const_name.is_empty() || !(const_name[0] >= 'A' && const_name[0] <= 'Z')) return false; - + if (r_constants.has(const_name)) return false; - + p_doc["name"] = const_name; - + int value_start = p_declaration.find("=", name_end); if (value_start != -1) { value_start++; @@ -443,7 +443,7 @@ static bool _try_extract_constant(const String& p_declaration, Dictionary& p_doc p_doc["value"] = p_declaration.substr(value_start, value_end - value_start).strip_edges(); } } - + r_constants[const_name] = p_doc; return true; } @@ -454,7 +454,7 @@ static void _extract_member_docs(const String& p_source, Dictionary& r_propertie static bool _try_extract_enum(const String& p_declaration, const String& p_source, int p_pos, Dictionary& p_doc, Dictionary& r_enums, Dictionary& r_constants) { if (!p_declaration.begins_with("enum ")) return false; - + int name_start = 5; while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { name_start++; @@ -466,7 +466,7 @@ static bool _try_extract_enum(const String& p_declaration, const String& p_sourc if (name_end > name_start) { String enum_name = p_declaration.substr(name_start, name_end - name_start); r_enums[enum_name] = p_doc; - + int body_start = p_source.find("{", p_pos); if (body_start != -1) { int body_end = p_source.find("}", body_start); @@ -474,7 +474,7 @@ static bool _try_extract_enum(const String& p_declaration, const String& p_sourc String enum_body = p_source.substr(body_start + 1, body_end - body_start - 1); Dictionary enum_props, enum_methods, enum_signals, enum_constants, enum_enums; _extract_member_docs(enum_body, enum_props, enum_methods, enum_signals, enum_constants, enum_enums); - + Array keys = enum_constants.keys(); for (int i = 0; i < keys.size(); i++) { Variant key = keys[i]; @@ -491,7 +491,7 @@ static bool _try_extract_enum(const String& p_declaration, const String& p_sourc static bool _try_extract_property(const String& p_declaration, Dictionary& p_doc, Dictionary& r_properties) { if (!(p_declaration.find("accessor") != -1 || (p_declaration.find(":") != -1 && p_declaration.find("(") == -1 && !p_declaration.begins_with("@")))) return false; - + int name_start = p_declaration.find("accessor") != -1 ? p_declaration.find("accessor") + 8 : 0; while (name_start < p_declaration.length() && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { name_start++; @@ -502,11 +502,11 @@ static bool _try_extract_property(const String& p_declaration, Dictionary& p_doc } if (name_end > name_start) { String prop_name = p_declaration.substr(name_start, name_end - name_start); - + if (r_properties.has(prop_name)) return false; - + p_doc["name"] = prop_name; - + int type_start = p_declaration.find(":", name_end); if (type_start != -1) { type_start++; @@ -521,7 +521,7 @@ static bool _try_extract_property(const String& p_declaration, Dictionary& p_doc p_doc["type"] = p_declaration.substr(type_start, type_end - type_start).strip_edges(); } } - + // Parse default value after = int value_start = p_declaration.find("=", name_end); if (value_start != -1) { @@ -537,7 +537,7 @@ static bool _try_extract_property(const String& p_declaration, Dictionary& p_doc p_doc["default_value"] = p_declaration.substr(value_start, value_end - value_start).strip_edges(); } } - + r_properties[prop_name] = p_doc; return true; } @@ -546,11 +546,11 @@ static bool _try_extract_property(const String& p_declaration, Dictionary& p_doc static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, Dictionary& r_methods) { if (p_declaration.find("(") == -1 || p_declaration.begins_with("@")) return false; - + Dictionary method_doc = p_doc; String qualifiers; String return_type; - + if (p_declaration.find("static ") != -1) { qualifiers = "static"; } @@ -558,7 +558,7 @@ static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, if (!qualifiers.is_empty()) qualifiers += " "; qualifiers += "async"; } - + int return_start = p_declaration.find("):"); if (return_start == -1) { return_start = p_declaration.find(") =>"); @@ -575,7 +575,7 @@ static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, return_type = p_declaration.substr(return_start, return_end - return_start).strip_edges(); } } - + int name_end = p_declaration.find("("); int name_start = name_end - 1; while (name_start >= 0 && (p_declaration[name_start] == ' ' || p_declaration[name_start] == '\t')) { @@ -597,7 +597,7 @@ static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, } else { method_doc["return_type"] = "void"; } - + int args_start = p_declaration.find("(") + 1; int args_end = p_declaration.find(")"); if (args_end > args_start) { @@ -608,18 +608,18 @@ static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, for (int i = 0; i < params.size(); i++) { String param = params[i].strip_edges(); if (param.is_empty()) continue; - + Dictionary arg_doc; String arg_name; String arg_type; String default_value; - + int eq_pos = param.find("="); if (eq_pos != -1) { default_value = param.substr(eq_pos + 1).strip_edges(); param = param.substr(0, eq_pos).strip_edges(); } - + int colon_pos = param.find(":"); if (colon_pos != -1) { arg_name = param.substr(0, colon_pos).strip_edges(); @@ -628,11 +628,11 @@ static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, arg_name = param; arg_type = "any"; } - + if (arg_name.ends_with("?")) { arg_name = arg_name.substr(0, arg_name.length() - 1); } - + arg_doc["name"] = arg_name; arg_doc["type"] = arg_type; if (!default_value.is_empty()) { @@ -645,7 +645,7 @@ static bool _try_extract_method(const String& p_declaration, Dictionary& p_doc, } } } - + r_methods[method_name] = method_doc; return true; } @@ -671,11 +671,11 @@ static void _extract_member_docs(const String& p_source, Dictionary& r_propertie // Get multiple lines to handle decorators on separate lines String remaining = p_source.substr(next_pos, 400); PackedStringArray lines = remaining.split("\n"); - + // Check first line for decorator String first_line = lines.size() > 0 ? lines[0].strip_edges() : ""; String declaration; - + // If first line is a decorator, check the next line for the actual declaration if (first_line.begins_with("@")) { declaration = first_line; @@ -713,81 +713,113 @@ Vector GodotJSScript::get_documentation() const String base_type; const String class_name = GodotJSScriptLanguage::get_singleton()->get_global_class_name(get_path(), &base_type); - - Dictionary class_dict; - class_dict["name"] = class_name; - class_dict["inherits"] = base_type.is_empty() ? "Object" : base_type; - class_dict["is_script_doc"] = true; - class_dict["script_path"] = get_path(); - - const String source = get_source_code(); - if (!source.is_empty()) { - Dictionary class_doc = _extract_class_doc(source, class_name); - if (class_doc.has("brief_description")) { - class_dict["brief_description"] = class_doc["brief_description"]; - } - if (class_doc.has("description")) { - class_dict["description"] = class_doc["description"]; - } - if (class_doc.has("tutorials")) { - class_dict["tutorials"] = class_doc["tutorials"]; - } - if (class_doc.has("deprecated")) { - class_dict["deprecated"] = class_doc["deprecated"]; - } - if (class_doc.has("experimental")) { - class_dict["experimental"] = class_doc["experimental"]; - } - Dictionary properties; - Dictionary methods; - Dictionary signals; - Dictionary constants; - Dictionary enums; - _extract_member_docs(source, properties, methods, signals, constants, enums); - - Array properties_array; - Array keys = properties.keys(); - for (int i = 0; i < keys.size(); i++) { - properties_array.push_back(properties[keys[i]]); - } - if (!properties_array.is_empty()) { - class_dict["properties"] = properties_array; - } - - Array methods_array; - keys = methods.keys(); - for (int i = 0; i < keys.size(); i++) { - methods_array.push_back(methods[keys[i]]); - } - if (!methods_array.is_empty()) { - class_dict["methods"] = methods_array; - } - - Array signals_array; - keys = signals.keys(); - for (int i = 0; i < keys.size(); i++) { - signals_array.push_back(signals[keys[i]]); - } - if (!signals_array.is_empty()) { - class_dict["signals"] = signals_array; - } - - Array constants_array; - keys = constants.keys(); - for (int i = 0; i < keys.size(); i++) { - constants_array.push_back(constants[keys[i]]); - } - if (!constants_array.is_empty()) { - class_dict["constants"] = constants_array; - } + DocData::ClassDoc class_doc_data; + + if (jsb::internal::Settings::get_jsdoc_documentation_comments()) { + Dictionary class_dict; + class_dict["name"] = class_name; + class_dict["inherits"] = base_type.is_empty() ? "Object" : base_type; + class_dict["is_script_doc"] = true; + class_dict["script_path"] = get_path(); + + const String source = get_source_code(); + if (!source.is_empty()) { + Dictionary class_doc = _extract_class_doc(source, class_name); + if (class_doc.has("brief_description")) { + class_dict["brief_description"] = class_doc["brief_description"]; + } + if (class_doc.has("description")) { + class_dict["description"] = class_doc["description"]; + } + if (class_doc.has("tutorials")) { + class_dict["tutorials"] = class_doc["tutorials"]; + } + if (class_doc.has("deprecated")) { + class_dict["deprecated"] = class_doc["deprecated"]; + } + if (class_doc.has("experimental")) { + class_dict["experimental"] = class_doc["experimental"]; + } + + Dictionary properties; + Dictionary methods; + Dictionary signals; + Dictionary constants; + Dictionary enums; + _extract_member_docs(source, properties, methods, signals, constants, enums); + + Array properties_array; + Array keys = properties.keys(); + for (int i = 0; i < keys.size(); i++) { + properties_array.push_back(properties[keys[i]]); + } + if (!properties_array.is_empty()) { + class_dict["properties"] = properties_array; + } + + Array methods_array; + keys = methods.keys(); + for (int i = 0; i < keys.size(); i++) { + methods_array.push_back(methods[keys[i]]); + } + if (!methods_array.is_empty()) { + class_dict["methods"] = methods_array; + } + + Array signals_array; + keys = signals.keys(); + for (int i = 0; i < keys.size(); i++) { + signals_array.push_back(signals[keys[i]]); + } + if (!signals_array.is_empty()) { + class_dict["signals"] = signals_array; + } + + Array constants_array; + keys = constants.keys(); + for (int i = 0; i < keys.size(); i++) { + constants_array.push_back(constants[keys[i]]); + } + if (!constants_array.is_empty()) { + class_dict["constants"] = constants_array; + } + + if (!enums.is_empty()) { + class_dict["enums"] = enums; + } + } + + class_doc_data = DocData::ClassDoc::from_dict(class_dict); + } + + + class_doc_data.name = class_name; + class_doc_data.inherits = base_type.is_empty() ? "Object" : base_type; + class_doc_data.is_script_doc = true; + class_doc_data.brief_description = script_class_info_.doc.brief_description; + class_doc_data.is_deprecated = script_class_info_.doc.is_deprecated; + class_doc_data.is_experimental = script_class_info_.doc.is_experimental; +#if GODOT_4_3_OR_NEWER + class_doc_data.deprecated_message = script_class_info_.doc.deprecated_message; + class_doc_data.experimental_message = script_class_info_.doc.experimental_message; +#endif + class_doc_data.script_path = get_path(); + for (const auto& item : script_class_info_.properties) + { + DocData::PropertyDoc property_doc_data; + property_doc_data.name = item.key; + property_doc_data.description = item.value.doc.brief_description; + property_doc_data.is_deprecated = item.value.doc.is_deprecated; + property_doc_data.is_experimental = item.value.doc.is_experimental; +#if GODOT_4_3_OR_NEWER + property_doc_data.deprecated_message = item.value.doc.deprecated_message; + property_doc_data.experimental_message = item.value.doc.experimental_message; +#endif + class_doc_data.properties.append(property_doc_data); + } - if (!enums.is_empty()) { - class_dict["enums"] = enums; - } - } - DocData::ClassDoc class_doc_data = DocData::ClassDoc::from_dict(class_dict); return { class_doc_data }; }