diff --git a/.changeset/silent-clouds-matter.md b/.changeset/silent-clouds-matter.md new file mode 100644 index 00000000..e07ddb5c --- /dev/null +++ b/.changeset/silent-clouds-matter.md @@ -0,0 +1,43 @@ +--- +"@godot-js/editor": minor +--- + +feat: additionally to @bind.help/experimental/deprecated add an editor setting to enable standard JS comments and JSDoc like annotations for documentation comments. + +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. + * + * 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/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/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..296dab78 100644 --- a/weaver/jsb_script.cpp +++ b/weaver/jsb_script.cpp @@ -246,10 +246,460 @@ 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); + + 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[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,32 +713,112 @@ 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; + + 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; + 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; + 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; + 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); - } + class_doc_data.properties.append(property_doc_data); + } + return { class_doc_data }; }