diff options
author | Yuri Sizov <11782833+YuriSizov@users.noreply.github.com> | 2023-04-26 16:54:25 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-26 16:54:25 +0200 |
commit | 26fb911f79d7b16c46ca476923fe1f32ab5d27ed (patch) | |
tree | f194bb48dea30dc8a18e3989b948f5f57d82232d | |
parent | e2e870c6118f6e9463c8907c947102f913f543de (diff) | |
parent | 6783ff69c06a5c6fcb0e22802fedcc1fa32ac25c (diff) | |
download | redot-engine-26fb911f79d7b16c46ca476923fe1f32ab5d27ed.tar.gz |
Merge pull request #72095 from anvilfolk/gd-docs
Improve GDScript documentation generation & behavior
-rw-r--r-- | core/doc_data.h | 55 | ||||
-rw-r--r-- | editor/editor_help.cpp | 124 | ||||
-rw-r--r-- | editor/editor_help.h | 4 | ||||
-rw-r--r-- | editor/plugins/script_editor_plugin.cpp | 2 | ||||
-rw-r--r-- | modules/gdscript/editor/gdscript_docgen.cpp | 271 | ||||
-rw-r--r-- | modules/gdscript/editor/gdscript_docgen.h | 42 | ||||
-rw-r--r-- | modules/gdscript/gdscript.cpp | 177 | ||||
-rw-r--r-- | modules/gdscript/gdscript.h | 10 | ||||
-rw-r--r-- | modules/gdscript/gdscript_compiler.cpp | 74 | ||||
-rw-r--r-- | modules/gdscript/gdscript_parser.cpp | 17 | ||||
-rw-r--r-- | modules/gdscript/gdscript_parser.h | 2 |
11 files changed, 431 insertions, 347 deletions
diff --git a/core/doc_data.h b/core/doc_data.h index d064818cd5..4e0db89984 100644 --- a/core/doc_data.h +++ b/core/doc_data.h @@ -315,61 +315,6 @@ public: } }; - struct EnumDoc { - String name = "@unnamed_enum"; - bool is_bitfield = false; - String description; - Vector<DocData::ConstantDoc> values; - static EnumDoc from_dict(const Dictionary &p_dict) { - EnumDoc doc; - - if (p_dict.has("name")) { - doc.name = p_dict["name"]; - } - - if (p_dict.has("is_bitfield")) { - doc.is_bitfield = p_dict["is_bitfield"]; - } - - if (p_dict.has("description")) { - doc.description = p_dict["description"]; - } - - Array values; - if (p_dict.has("values")) { - values = p_dict["values"]; - } - for (int i = 0; i < values.size(); i++) { - doc.values.push_back(ConstantDoc::from_dict(values[i])); - } - - return doc; - } - static Dictionary to_dict(const EnumDoc &p_doc) { - Dictionary dict; - - if (!p_doc.name.is_empty()) { - dict["name"] = p_doc.name; - } - - dict["is_bitfield"] = p_doc.is_bitfield; - - if (!p_doc.description.is_empty()) { - dict["description"] = p_doc.description; - } - - if (!p_doc.values.is_empty()) { - Array values; - for (int i = 0; i < p_doc.values.size(); i++) { - values.push_back(ConstantDoc::to_dict(p_doc.values[i])); - } - dict["values"] = values; - } - - return dict; - } - }; - struct PropertyDoc { String name; String type; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index b9634a7e03..f2d852cc2b 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -32,6 +32,7 @@ #include "core/core_constants.h" #include "core/input/input.h" +#include "core/object/script_language.h" #include "core/os/keyboard.h" #include "core/version.h" #include "doc_data_compressed.gen.h" @@ -44,6 +45,8 @@ #define CONTRIBUTE_URL vformat("%s/contributing/documentation/updating_the_class_reference.html", VERSION_DOCS_URL) +// TODO: this is sometimes used directly as doc->something, other times as EditorHelp::get_doc_data(), which is thread-safe. +// Might this be a problem? DocTools *EditorHelp::doc = nullptr; class DocCache : public Resource { @@ -73,6 +76,49 @@ public: void set_classes(const Array &p_classes) { classes = p_classes; } }; +static bool _attempt_doc_load(const String &p_class) { + // Docgen always happens in the outer-most class: it also generates docs for inner classes. + String outer_class = p_class.get_slice(".", 0); + if (!ScriptServer::is_global_class(outer_class)) { + return false; + } + + // ResourceLoader is used in order to have a script-agnostic way to load scripts. + // This forces GDScript to compile the code, which is unnecessary for docgen, but it's a good compromise right now. + Ref<Script> script = ResourceLoader::load(ScriptServer::get_global_class_path(outer_class), outer_class); + if (script.is_valid()) { + Vector<DocData::ClassDoc> docs = script->get_documentation(); + for (int j = 0; j < docs.size(); j++) { + const DocData::ClassDoc &doc = docs.get(j); + EditorHelp::get_doc_data()->add_doc(doc); + } + return true; + } + + return false; +} + +// Removes unnecessary prefix from p_class_specifier when within the p_edited_class context +static String _contextualize_class_specifier(const String &p_class_specifier, const String &p_edited_class) { + // If this is a completely different context than the current class, then keep full path + if (!p_class_specifier.begins_with(p_edited_class)) { + return p_class_specifier; + } + + // Here equal length + begins_with from above implies p_class_specifier == p_edited_class :) + if (p_class_specifier.length() == p_edited_class.length()) { + int rfind = p_class_specifier.rfind("."); + if (rfind == -1) { // Single identifier + return p_class_specifier; + } + // Multiple specifiers: keep last one only + return p_class_specifier.substr(rfind + 1); + } + + // Remove prefix + return p_class_specifier.substr(p_edited_class.length() + 1); +} + void EditorHelp::_update_theme_item_cache() { VBoxContainer::_update_theme_item_cache(); @@ -130,12 +176,13 @@ void EditorHelp::_class_list_select(const String &p_select) { } void EditorHelp::_class_desc_select(const String &p_select) { - if (p_select.begins_with("$")) { //enum + if (p_select.begins_with("$")) { // enum String select = p_select.substr(1, p_select.length()); String class_name; - if (select.contains(".")) { - class_name = select.get_slice(".", 0); - select = select.get_slice(".", 1); + int rfind = select.rfind("."); + if (rfind != -1) { + class_name = select.substr(0, rfind); + select = select.substr(rfind + 1); } else { class_name = "@GlobalScope"; } @@ -253,35 +300,35 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum) { bool is_enum_type = !p_enum.is_empty(); bool can_ref = !p_type.contains("*") || is_enum_type; - String t = p_type; + String link_t = p_type; // For links in metadata + String display_t = link_t; // For display purposes if (is_enum_type) { - if (p_enum.get_slice_count(".") > 1) { - t = p_enum.get_slice(".", 1); - } else { - t = p_enum.get_slice(".", 0); - } + link_t = p_enum; // The link for enums is always the full enum description + display_t = _contextualize_class_specifier(p_enum, edited_class); + } else { + display_t = _contextualize_class_specifier(p_type, edited_class); } class_desc->push_color(theme_cache.type_color); bool add_array = false; if (can_ref) { - if (t.ends_with("[]")) { + if (link_t.ends_with("[]")) { add_array = true; - t = t.replace("[]", ""); + link_t = link_t.replace("[]", ""); - class_desc->push_meta("#Array"); //class + class_desc->push_meta("#Array"); // class class_desc->add_text("Array"); class_desc->pop(); class_desc->add_text("["); } if (is_enum_type) { - class_desc->push_meta("$" + p_enum); //class + class_desc->push_meta("$" + link_t); // enum } else { - class_desc->push_meta("#" + t); //class + class_desc->push_meta("#" + link_t); // class } } - class_desc->add_text(t); + class_desc->add_text(display_t); if (can_ref) { class_desc->pop(); // Pushed meta above. if (add_array) { @@ -338,7 +385,7 @@ String EditorHelp::_fix_constant(const String &p_constant) const { class_desc->pop(); void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview) { - method_line[p_method.name] = class_desc->get_paragraph_count() - 2; //gets overridden if description + method_line[p_method.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description const bool is_vararg = p_method.qualifiers.contains("vararg"); @@ -352,8 +399,8 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview _add_type(p_method.return_type, p_method.return_enum); if (p_overview) { - class_desc->pop(); //align - class_desc->pop(); //cell + class_desc->pop(); // align + class_desc->pop(); // cell class_desc->push_cell(); } else { class_desc->add_text(" "); @@ -368,7 +415,7 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview class_desc->pop(); if (p_overview && !p_method.description.strip_edges().is_empty()) { - class_desc->pop(); //meta + class_desc->pop(); // meta } class_desc->push_color(theme_cache.symbol_color); @@ -447,7 +494,7 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview } if (p_overview) { - class_desc->pop(); //cell + class_desc->pop(); // cell } } @@ -488,8 +535,9 @@ void EditorHelp::_pop_code_font() { class_desc->pop(); } -Error EditorHelp::_goto_desc(const String &p_class, int p_vscr) { - if (!doc->class_list.has(p_class)) { +Error EditorHelp::_goto_desc(const String &p_class) { + // If class doesn't have docs listed, attempt on-demand docgen + if (!doc->class_list.has(p_class) && !_attempt_doc_load(p_class)) { return ERR_DOES_NOT_EXIST; } @@ -529,9 +577,9 @@ void EditorHelp::_update_method_list(const Vector<DocData::MethodDoc> p_methods) if (any_previous && !m.is_empty()) { class_desc->push_cell(); - class_desc->pop(); //cell + class_desc->pop(); // cell class_desc->push_cell(); - class_desc->pop(); //cell + class_desc->pop(); // cell } String group_prefix; @@ -549,9 +597,9 @@ void EditorHelp::_update_method_list(const Vector<DocData::MethodDoc> p_methods) if (is_new_group && pass == 1) { class_desc->push_cell(); - class_desc->pop(); //cell + class_desc->pop(); // cell class_desc->push_cell(); - class_desc->pop(); //cell + class_desc->pop(); // cell } _add_method(m[i], true); @@ -560,7 +608,7 @@ void EditorHelp::_update_method_list(const Vector<DocData::MethodDoc> p_methods) any_previous = !m.is_empty(); } - class_desc->pop(); //table + class_desc->pop(); // table class_desc->pop(); _pop_code_font(); @@ -1196,7 +1244,7 @@ void EditorHelp::_update_doc() { _add_text(cd.signals[i].arguments[j].name); class_desc->add_text(": "); - _add_type(cd.signals[i].arguments[j].type); + _add_type(cd.signals[i].arguments[j].type, cd.signals[i].arguments[j].enumeration); if (!cd.signals[i].arguments[j].default_value.is_empty()) { class_desc->push_color(theme_cache.symbol_color); class_desc->add_text(" = "); @@ -1767,7 +1815,6 @@ void EditorHelp::_request_help(const String &p_string) { if (err == OK) { EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); } - //100 palabras } void EditorHelp::_help_callback(const String &p_topic) { @@ -2178,7 +2225,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control tag_stack.push_front("font"); } else { - p_rt->add_text("["); //ignore + p_rt->add_text("["); // ignore pos = brk_pos + 1; } } @@ -2310,9 +2357,9 @@ void EditorHelp::go_to_help(const String &p_help) { _help_callback(p_help); } -void EditorHelp::go_to_class(const String &p_class, int p_scroll) { +void EditorHelp::go_to_class(const String &p_class) { _wait_for_thread(); - _goto_desc(p_class, p_scroll); + _goto_desc(p_class); } void EditorHelp::update_doc() { @@ -2443,14 +2490,15 @@ void EditorHelpBit::_go_to_help(String p_what) { } void EditorHelpBit::_meta_clicked(String p_select) { - if (p_select.begins_with("$")) { //enum - + if (p_select.begins_with("$")) { // enum String select = p_select.substr(1, p_select.length()); String class_name; - if (select.contains(".")) { - class_name = select.get_slice(".", 0); + int rfind = select.rfind("."); + if (rfind != -1) { + class_name = select.substr(0, rfind); + select = select.substr(rfind + 1); } else { - class_name = "@Global"; + class_name = "@GlobalScope"; } _go_to_help("class_enum:" + class_name + ":" + select); return; diff --git a/editor/editor_help.h b/editor/editor_help.h index 01e91b4593..4175ece816 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -178,7 +178,7 @@ class EditorHelp : public VBoxContainer { void _class_desc_resized(bool p_force_update_theme); int display_margin = 0; - Error _goto_desc(const String &p_class, int p_vscr = -1); + Error _goto_desc(const String &p_class); //void _update_history_buttons(); void _update_method_list(const Vector<DocData::MethodDoc> p_methods); void _update_method_descriptions(const DocData::ClassDoc p_classdoc, const Vector<DocData::MethodDoc> p_methods, const String &p_method_type); @@ -210,7 +210,7 @@ public: static String get_cache_full_path(); void go_to_help(const String &p_help); - void go_to_class(const String &p_class, int p_scroll = 0); + void go_to_class(const String &p_class); void update_doc(); Vector<Pair<String, int>> get_sections(); diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 7be4d76f1e..e928abe00e 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -3316,7 +3316,7 @@ void ScriptEditor::_help_class_open(const String &p_class) { eh->set_name(p_class); tab_container->add_child(eh); _go_to_tab(tab_container->get_tab_count() - 1); - eh->go_to_class(p_class, 0); + eh->go_to_class(p_class); eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto)); _add_recent_script(p_class); _sort_list_on_update = true; diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp new file mode 100644 index 0000000000..451af996ec --- /dev/null +++ b/modules/gdscript/editor/gdscript_docgen.cpp @@ -0,0 +1,271 @@ +/**************************************************************************/ +/* gdscript_docgen.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_docgen.h" +#include "../gdscript.h" + +using GDP = GDScriptParser; +using GDType = GDP::DataType; + +static String _get_script_path(const String &p_path) { + return vformat(R"("%s")", p_path.get_slice("://", 1)); +} + +static String _get_class_name(const GDP::ClassNode &p_class) { + const GDP::ClassNode *curr_class = &p_class; + if (!curr_class->identifier) { // All inner classes have a identifier, so this is the outer class + return _get_script_path(curr_class->fqcn); + } + + String full_name = curr_class->identifier->name; + while (curr_class->outer) { + curr_class = curr_class->outer; + if (!curr_class->identifier) { // All inner classes have a identifier, so this is the outer class + return vformat("%s.%s", _get_script_path(curr_class->fqcn), full_name); + } + full_name = vformat("%s.%s", curr_class->identifier->name, full_name); + } + return full_name; +} + +static PropertyInfo _property_info_from_datatype(const GDType &p_type) { + PropertyInfo pi; + pi.type = p_type.builtin_type; + if (p_type.kind == GDType::CLASS) { + pi.class_name = _get_class_name(*p_type.class_type); + } else if (p_type.kind == GDType::ENUM && p_type.enum_type != StringName()) { + pi.type = Variant::INT; // Only int types are recognized as enums by the EditorHelp + pi.usage |= PROPERTY_USAGE_CLASS_IS_ENUM; + // Replace :: from enum's use of fully qualified class names with regular . + pi.class_name = String(p_type.native_type).replace("::", "."); + } else if (p_type.kind == GDType::NATIVE) { + pi.class_name = p_type.native_type; + } + return pi; +} + +void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_class) { + p_script->_clear_doc(); + + DocData::ClassDoc &doc = p_script->doc; + + doc.script_path = _get_script_path(p_script->get_script_path()); + if (p_script->name.is_empty()) { + doc.name = doc.script_path; + } else { + doc.name = p_script->name; + } + + if (p_script->_owner) { + doc.name = p_script->_owner->doc.name + "." + doc.name; + doc.script_path = doc.script_path + "." + doc.name; + } + + doc.is_script_doc = true; + + if (p_script->base.is_valid() && p_script->base->is_valid()) { + if (!p_script->base->doc.name.is_empty()) { + doc.inherits = p_script->base->doc.name; + } else { + doc.inherits = p_script->base->get_instance_base_type(); + } + } else if (p_script->native.is_valid()) { + doc.inherits = p_script->native->get_name(); + } + + doc.brief_description = p_class->doc_brief_description; + doc.description = p_class->doc_description; + for (const Pair<String, String> &p : p_class->doc_tutorials) { + DocData::TutorialDoc td; + td.title = p.first; + td.link = p.second; + doc.tutorials.append(td); + } + + for (const GDP::ClassNode::Member &member : p_class->members) { + switch (member.type) { + case GDP::ClassNode::Member::CLASS: { + const GDP::ClassNode *inner_class = member.m_class; + const StringName &class_name = inner_class->identifier->name; + + p_script->member_lines[class_name] = inner_class->start_line; + + // Recursively generate inner class docs + // Needs inner GDScripts to exist: previously generated in GDScriptCompiler::make_scripts() + GDScriptDocGen::generate_docs(*p_script->subclasses[class_name], inner_class); + } break; + + case GDP::ClassNode::Member::CONSTANT: { + const GDP::ConstantNode *m_const = member.constant; + const StringName &const_name = member.constant->identifier->name; + + p_script->member_lines[const_name] = m_const->start_line; + + DocData::ConstantDoc const_doc; + DocData::constant_doc_from_variant(const_doc, const_name, m_const->initializer->reduced_value, m_const->doc_description); + doc.constants.push_back(const_doc); + } break; + + case GDP::ClassNode::Member::FUNCTION: { + const GDP::FunctionNode *m_func = member.function; + const StringName &func_name = m_func->identifier->name; + + p_script->member_lines[func_name] = m_func->start_line; + + MethodInfo mi; + mi.name = func_name; + + if (m_func->return_type) { + mi.return_val = _property_info_from_datatype(m_func->return_type->get_datatype()); + } + for (const GDScriptParser::ParameterNode *p : m_func->parameters) { + PropertyInfo pi = _property_info_from_datatype(p->get_datatype()); + pi.name = p->identifier->name; + mi.arguments.push_back(pi); + } + + DocData::MethodDoc method_doc; + DocData::method_doc_from_methodinfo(method_doc, mi, m_func->doc_description); + doc.methods.push_back(method_doc); + } break; + + case GDP::ClassNode::Member::SIGNAL: { + const GDP::SignalNode *m_signal = member.signal; + const StringName &signal_name = m_signal->identifier->name; + + p_script->member_lines[signal_name] = m_signal->start_line; + + MethodInfo mi; + mi.name = signal_name; + for (const GDScriptParser::ParameterNode *p : m_signal->parameters) { + PropertyInfo pi = _property_info_from_datatype(p->get_datatype()); + pi.name = p->identifier->name; + mi.arguments.push_back(pi); + } + + DocData::MethodDoc signal_doc; + DocData::signal_doc_from_methodinfo(signal_doc, mi, m_signal->doc_description); + doc.signals.push_back(signal_doc); + } break; + + case GDP::ClassNode::Member::VARIABLE: { + const GDP::VariableNode *m_var = member.variable; + const StringName &var_name = m_var->identifier->name; + + p_script->member_lines[var_name] = m_var->start_line; + + DocData::PropertyDoc prop_doc; + + prop_doc.name = var_name; + prop_doc.description = m_var->doc_description; + + GDType dt = m_var->get_datatype(); + switch (dt.kind) { + case GDType::CLASS: + prop_doc.type = _get_class_name(*dt.class_type); + break; + case GDType::VARIANT: + prop_doc.type = "Variant"; + break; + case GDType::ENUM: + prop_doc.type = Variant::get_type_name(dt.builtin_type); + // Replace :: from enum's use of fully qualified class names with regular . + prop_doc.enumeration = String(dt.native_type).replace("::", "."); + break; + case GDType::NATIVE:; + prop_doc.type = dt.native_type; + break; + case GDType::BUILTIN: + prop_doc.type = Variant::get_type_name(dt.builtin_type); + break; + default: + // SCRIPT: can be preload()'d and perhaps used as types directly? + // RESOLVING & UNRESOLVED should never happen since docgen requires analyzing w/o errors + break; + } + + if (m_var->property == GDP::VariableNode::PROP_SETGET) { + if (m_var->setter_pointer != nullptr) { + prop_doc.setter = m_var->setter_pointer->name; + } + if (m_var->getter_pointer != nullptr) { + prop_doc.getter = m_var->getter_pointer->name; + } + } + + if (m_var->initializer && m_var->initializer->is_constant) { + prop_doc.default_value = m_var->initializer->reduced_value.get_construct_string().replace("\n", ""); + } + + prop_doc.overridden = false; + + doc.properties.push_back(prop_doc); + } break; + + case GDP::ClassNode::Member::ENUM: { + const GDP::EnumNode *m_enum = member.m_enum; + StringName name = m_enum->identifier->name; + + p_script->member_lines[name] = m_enum->start_line; + + for (const GDP::EnumNode::Value &val : m_enum->values) { + DocData::ConstantDoc const_doc; + const_doc.name = val.identifier->name; + const_doc.value = String(Variant(val.value)); + const_doc.description = val.doc_description; + const_doc.enumeration = name; + + doc.enums[const_doc.name] = const_doc.description; + doc.constants.push_back(const_doc); + } + + } break; + + case GDP::ClassNode::Member::ENUM_VALUE: { + const GDP::EnumNode::Value &m_enum_val = member.enum_value; + const StringName &name = m_enum_val.identifier->name; + + p_script->member_lines[name] = m_enum_val.identifier->start_line; + + DocData::ConstantDoc constant_doc; + constant_doc.enumeration = "@unnamed_enums"; + DocData::constant_doc_from_variant(constant_doc, name, m_enum_val.value, m_enum_val.doc_description); + doc.constants.push_back(constant_doc); + } break; + case GDP::ClassNode::Member::GROUP: + case GDP::ClassNode::Member::UNDEFINED: + default: + break; + } + } + + // Add doc to the outer-most class. + p_script->_add_doc(doc); +} diff --git a/modules/gdscript/editor/gdscript_docgen.h b/modules/gdscript/editor/gdscript_docgen.h new file mode 100644 index 0000000000..bb3647196a --- /dev/null +++ b/modules/gdscript/editor/gdscript_docgen.h @@ -0,0 +1,42 @@ +/**************************************************************************/ +/* gdscript_docgen.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_DOCGEN_H +#define GDSCRIPT_DOCGEN_H + +#include "../gdscript_parser.h" +#include "core/doc_data.h" + +class GDScriptDocGen { +public: + static void generate_docs(GDScript *p_script, const GDScriptParser::ClassNode *p_class); +}; + +#endif // GDSCRIPT_DOCGEN_H diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 2646c1ad15..e74c2866e6 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -52,6 +52,7 @@ #ifdef TOOLS_ENABLED #include "editor/editor_paths.h" +#include "editor/gdscript_docgen.h" #endif /////////////////////////// @@ -340,12 +341,11 @@ void GDScript::_get_script_property_list(List<PropertyInfo> *r_list, bool p_incl r_list->push_back(E); } - props.clear(); - if (!p_include_base) { break; } + props.clear(); sptr = sptr->_base; } } @@ -461,9 +461,9 @@ void GDScript::_update_exports_values(HashMap<StringName, Variant> &values, List } void GDScript::_add_doc(const DocData::ClassDoc &p_inner_class) { - if (_owner) { + if (_owner) { // Only the top-level class stores doc info _owner->_add_doc(p_inner_class); - } else { + } else { // Remove old docs, add new for (int i = 0; i < docs.size(); i++) { if (docs[i].name == p_inner_class.name) { docs.remove_at(i); @@ -478,167 +478,6 @@ void GDScript::_clear_doc() { docs.clear(); doc = DocData::ClassDoc(); } - -void GDScript::_update_doc() { - _clear_doc(); - - doc.script_path = vformat(R"("%s")", get_script_path().get_slice("://", 1)); - if (!name.is_empty()) { - doc.name = name; - } else { - doc.name = doc.script_path; - } - - if (_owner) { - doc.name = _owner->doc.name + "." + doc.name; - doc.script_path = doc.script_path + "." + doc.name; - } - - doc.is_script_doc = true; - - if (base.is_valid() && base->is_valid()) { - if (!base->doc.name.is_empty()) { - doc.inherits = base->doc.name; - } else { - doc.inherits = base->get_instance_base_type(); - } - } else if (native.is_valid()) { - doc.inherits = native->get_name(); - } - - doc.brief_description = doc_brief_description; - doc.description = doc_description; - doc.tutorials = doc_tutorials; - - for (const KeyValue<String, DocData::EnumDoc> &E : doc_enums) { - if (!E.value.description.is_empty()) { - doc.enums[E.key] = E.value.description; - } - } - - List<MethodInfo> methods; - _get_script_method_list(&methods, false); - for (int i = 0; i < methods.size(); i++) { - // Ignore internal methods. - if (methods[i].name[0] == '@') { - continue; - } - - DocData::MethodDoc method_doc; - const String &class_name = methods[i].name; - if (member_functions.has(class_name)) { - GDScriptFunction *fn = member_functions[class_name]; - - // Change class name if return type is script reference. - GDScriptDataType return_type = fn->get_return_type(); - if (return_type.kind == GDScriptDataType::GDSCRIPT) { - methods[i].return_val.class_name = _get_gdscript_reference_class_name(Object::cast_to<GDScript>(return_type.script_type)); - } - - // Change class name if argument is script reference. - for (int j = 0; j < fn->get_argument_count(); j++) { - GDScriptDataType arg_type = fn->get_argument_type(j); - if (arg_type.kind == GDScriptDataType::GDSCRIPT) { - methods[i].arguments[j].class_name = _get_gdscript_reference_class_name(Object::cast_to<GDScript>(arg_type.script_type)); - } - } - } - if (doc_functions.has(methods[i].name)) { - DocData::method_doc_from_methodinfo(method_doc, methods[i], doc_functions[methods[i].name]); - } else { - DocData::method_doc_from_methodinfo(method_doc, methods[i], String()); - } - doc.methods.push_back(method_doc); - } - - List<PropertyInfo> props; - _get_script_property_list(&props, false); - for (int i = 0; i < props.size(); i++) { - if (props[i].usage & PROPERTY_USAGE_CATEGORY || props[i].usage & PROPERTY_USAGE_GROUP || props[i].usage & PROPERTY_USAGE_SUBGROUP) { - continue; - } - ScriptMemberInfo scr_member_info; - scr_member_info.propinfo = props[i]; - scr_member_info.propinfo.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; - if (member_indices.has(props[i].name)) { - const MemberInfo &mi = member_indices[props[i].name]; - scr_member_info.setter = mi.setter; - scr_member_info.getter = mi.getter; - if (mi.data_type.kind == GDScriptDataType::GDSCRIPT) { - scr_member_info.propinfo.class_name = _get_gdscript_reference_class_name( - Object::cast_to<GDScript>(mi.data_type.script_type)); - } - } - if (member_default_values.has(props[i].name)) { - scr_member_info.has_default_value = true; - scr_member_info.default_value = member_default_values[props[i].name]; - } - if (doc_variables.has(props[i].name)) { - scr_member_info.doc_string = doc_variables[props[i].name]; - } - - DocData::PropertyDoc prop_doc; - DocData::property_doc_from_scriptmemberinfo(prop_doc, scr_member_info); - doc.properties.push_back(prop_doc); - } - - List<MethodInfo> signals; - _get_script_signal_list(&signals, false); - for (int i = 0; i < signals.size(); i++) { - DocData::MethodDoc signal_doc; - if (doc_signals.has(signals[i].name)) { - DocData::signal_doc_from_methodinfo(signal_doc, signals[i], doc_signals[signals[i].name]); - } else { - DocData::signal_doc_from_methodinfo(signal_doc, signals[i], String()); - } - doc.signals.push_back(signal_doc); - } - - for (const KeyValue<StringName, Variant> &E : constants) { - if (subclasses.has(E.key)) { - continue; - } - - // Enums. - bool is_enum = false; - if (E.value.get_type() == Variant::DICTIONARY) { - if (doc_enums.has(E.key)) { - is_enum = true; - for (int i = 0; i < doc_enums[E.key].values.size(); i++) { - doc_enums[E.key].values.write[i].enumeration = E.key; - doc.constants.push_back(doc_enums[E.key].values[i]); - } - } - } - if (!is_enum && doc_enums.has("@unnamed_enums")) { - for (int i = 0; i < doc_enums["@unnamed_enums"].values.size(); i++) { - if (E.key == doc_enums["@unnamed_enums"].values[i].name) { - is_enum = true; - DocData::ConstantDoc constant_doc; - constant_doc.enumeration = "@unnamed_enums"; - DocData::constant_doc_from_variant(constant_doc, E.key, E.value, doc_enums["@unnamed_enums"].values[i].description); - doc.constants.push_back(constant_doc); - break; - } - } - } - if (!is_enum) { - DocData::ConstantDoc constant_doc; - String const_description; - if (doc_constants.has(E.key)) { - const_description = doc_constants[E.key]; - } - DocData::constant_doc_from_variant(constant_doc, E.key, E.value, const_description); - doc.constants.push_back(constant_doc); - } - } - - for (KeyValue<StringName, Ref<GDScript>> &E : subclasses) { - E.value->_update_doc(); - } - - _add_doc(doc); -} #endif bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderScriptInstance *p_instance_to_update) { @@ -905,6 +744,13 @@ Error GDScript::reload(bool p_keep_state) { return err; } } + +#ifdef TOOLS_ENABLED + // Done after compilation because it needs the GDScript object's inner class GDScript objects, + // which are made by calling make_scripts() within compiler.compile() above. + GDScriptDocGen::generate_docs(this, parser.get_tree()); +#endif + #ifdef DEBUG_ENABLED for (const GDScriptWarning &warning : parser.get_warnings()) { if (EngineDebugger::is_active()) { @@ -1266,7 +1112,6 @@ void GDScript::_get_script_signal_list(List<MethodInfo> *r_list, bool p_include_ else if (base_cache.is_valid()) { base_cache->get_script_signal_list(r_list); } - #endif } diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 0117ed40ab..7ba6368947 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -83,6 +83,7 @@ class GDScript : public Script { friend class GDScriptFunction; friend class GDScriptAnalyzer; friend class GDScriptCompiler; + friend class GDScriptDocGen; friend class GDScriptLanguage; friend struct GDScriptUtilityFunctionsDefinitions; @@ -113,16 +114,7 @@ class GDScript : public Script { DocData::ClassDoc doc; Vector<DocData::ClassDoc> docs; - String doc_brief_description; - String doc_description; - Vector<DocData::TutorialDoc> doc_tutorials; - HashMap<String, String> doc_functions; - HashMap<String, String> doc_variables; - HashMap<String, String> doc_constants; - HashMap<String, String> doc_signals; - HashMap<String, DocData::EnumDoc> doc_enums; void _clear_doc(); - void _update_doc(); void _add_doc(const DocData::ClassDoc &p_inner_class); #endif diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 42619a12a8..43008a5d93 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -2145,12 +2145,6 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_ if (p_func) { codegen.generator->set_initial_line(p_func->start_line); -#ifdef TOOLS_ENABLED - if (!p_for_lambda) { - p_script->member_lines[func_name] = p_func->start_line; - p_script->doc_functions[func_name] = p_func->doc_description; - } -#endif } else { codegen.generator->set_initial_line(0); } @@ -2219,23 +2213,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri parsing_classes.insert(p_script); p_script->clearing = true; -#ifdef TOOLS_ENABLED - p_script->doc_functions.clear(); - p_script->doc_variables.clear(); - p_script->doc_constants.clear(); - p_script->doc_enums.clear(); - p_script->doc_signals.clear(); - p_script->doc_tutorials.clear(); - - p_script->doc_brief_description = p_class->doc_brief_description; - p_script->doc_description = p_class->doc_description; - for (int i = 0; i < p_class->doc_tutorials.size(); i++) { - DocData::TutorialDoc td; - td.title = p_class->doc_tutorials[i].first; - td.link = p_class->doc_tutorials[i].second; - p_script->doc_tutorials.append(td); - } -#endif p_script->native = Ref<GDScriptNativeClass>(); p_script->base = Ref<GDScript>(); @@ -2379,9 +2356,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri } else { prop_info.usage = PROPERTY_USAGE_SCRIPT_VARIABLE; } -#ifdef TOOLS_ENABLED - p_script->doc_variables[name] = variable->doc_description; -#endif p_script->member_info[name] = prop_info; p_script->member_indices[name] = minfo; @@ -2394,7 +2368,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri } else { p_script->member_default_values.erase(name); } - p_script->member_lines[name] = variable->start_line; #endif } break; @@ -2403,12 +2376,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri StringName name = constant->identifier->name; p_script->constants.insert(name, constant->initializer->reduced_value); -#ifdef TOOLS_ENABLED - p_script->member_lines[name] = constant->start_line; - if (!constant->doc_description.is_empty()) { - p_script->doc_constants[name] = constant->doc_description; - } -#endif } break; case GDScriptParser::ClassNode::Member::ENUM_VALUE: { @@ -2416,18 +2383,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri StringName name = enum_value.identifier->name; p_script->constants.insert(name, enum_value.value); -#ifdef TOOLS_ENABLED - p_script->member_lines[name] = enum_value.identifier->start_line; - if (!p_script->doc_enums.has("@unnamed_enums")) { - p_script->doc_enums["@unnamed_enums"] = DocData::EnumDoc(); - p_script->doc_enums["@unnamed_enums"].name = "@unnamed_enums"; - } - DocData::ConstantDoc const_doc; - const_doc.name = enum_value.identifier->name; - const_doc.value = Variant(enum_value.value).operator String(); // TODO-DOC: enum value currently is int. - const_doc.description = enum_value.doc_description; - p_script->doc_enums["@unnamed_enums"].values.push_back(const_doc); -#endif } break; case GDScriptParser::ClassNode::Member::SIGNAL: { @@ -2440,11 +2395,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri parameters_names.write[j] = signal->parameters[j]->identifier->name; } p_script->_signals[name] = parameters_names; -#ifdef TOOLS_ENABLED - if (!signal->doc_description.is_empty()) { - p_script->doc_signals[name] = signal->doc_description; - } -#endif } break; case GDScriptParser::ClassNode::Member::ENUM: { @@ -2452,19 +2402,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri StringName name = enum_n->identifier->name; p_script->constants.insert(name, enum_n->dictionary); -#ifdef TOOLS_ENABLED - p_script->member_lines[name] = enum_n->start_line; - p_script->doc_enums[name] = DocData::EnumDoc(); - p_script->doc_enums[name].name = name; - p_script->doc_enums[name].description = enum_n->doc_description; - for (int j = 0; j < enum_n->values.size(); j++) { - DocData::ConstantDoc const_doc; - const_doc.name = enum_n->values[j].identifier->name; - const_doc.value = Variant(enum_n->values[j].value).operator String(); - const_doc.description = enum_n->values[j].doc_description; - p_script->doc_enums[name].values.push_back(const_doc); - } -#endif } break; case GDScriptParser::ClassNode::Member::GROUP: { @@ -2512,9 +2449,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri } } -#ifdef TOOLS_ENABLED - p_script->member_lines[name] = inner_class->start_line; -#endif p_script->constants.insert(name, subclass); //once parsed, goes to the list of constants } @@ -2607,7 +2541,7 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser: //well, tough luck, not gonna do anything here } } -#endif +#endif // TOOLS_ENABLED } else { GDScriptInstance *gi = static_cast<GDScriptInstance *>(si); gi->reload_members(); @@ -2616,7 +2550,7 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser: E = N; } } -#endif +#endif //DEBUG_ENABLED for (int i = 0; i < p_class->members.size(); i++) { if (p_class->members[i].type != GDScriptParser::ClassNode::Member::CLASS) { @@ -2716,10 +2650,6 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri return err; } -#ifdef TOOLS_ENABLED - p_script->_update_doc(); -#endif - return GDScriptCache::finish_compiling(main_script->get_path()); } diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index bcc116cda2..6a4a4e66f2 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -761,7 +761,11 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)() #ifdef TOOLS_ENABLED // Consume doc comments. class_doc_line = MIN(class_doc_line, doc_comment_line - 1); - if (has_comment(doc_comment_line)) { + + // Check whether current line has a doc comment + if (has_comment(previous.start_line, true)) { + member->doc_description = get_doc_comment(previous.start_line, true); + } else if (has_comment(doc_comment_line, true)) { if constexpr (std::is_same_v<T, ClassNode>) { get_class_doc_comment(doc_comment_line, member->doc_brief_description, member->doc_description, member->doc_tutorials, true); } else { @@ -3296,8 +3300,15 @@ static bool _in_codeblock(String p_line, bool p_already_in, int *r_block_begins } } -bool GDScriptParser::has_comment(int p_line) { - return tokenizer.get_comments().has(p_line); +bool GDScriptParser::has_comment(int p_line, bool p_must_be_doc) { + bool has_comment = tokenizer.get_comments().has(p_line); + // If there are no comments or if we don't care whether the comment + // is a docstring, we have our result. + if (!p_must_be_doc || !has_comment) { + return has_comment; + } + + return tokenizer.get_comments()[p_line].comment.begins_with("##"); } String GDScriptParser::get_doc_comment(int p_line, bool p_single_line) { diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 29841ab060..d85a527406 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1480,7 +1480,7 @@ private: #ifdef TOOLS_ENABLED // Doc comments. int class_doc_line = 0x7FFFFFFF; - bool has_comment(int p_line); + bool has_comment(int p_line, bool p_must_be_doc = false); String get_doc_comment(int p_line, bool p_single_line = false); void get_class_doc_comment(int p_line, String &p_brief, String &p_desc, Vector<Pair<String, String>> &p_tutorials, bool p_inner_class); #endif // TOOLS_ENABLED |