Browse Source

Improve and fix GDScript documentation generation & behavior

Removes documentation generation (docgen) from the GDScript compiler to
its own file. Adds support for GDScript enums and signal parameters and
quite a few other assorted fixes and improvements.
ocean (they/them) 2 years ago
parent
commit
6783ff69c0

+ 0 - 55
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;

+ 86 - 38
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 "core/version_generated.gen.h"
@@ -45,6 +46,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 {
@@ -74,6 +77,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();
 
@@ -131,12 +177,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";
 		}
@@ -254,35 +301,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) {
@@ -339,7 +386,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");
 
@@ -353,8 +400,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(" ");
@@ -369,7 +416,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);
@@ -448,7 +495,7 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview
 	}
 
 	if (p_overview) {
-		class_desc->pop(); //cell
+		class_desc->pop(); // cell
 	}
 }
 
@@ -489,8 +536,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;
 	}
 
@@ -530,9 +578,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;
@@ -550,9 +598,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);
@@ -561,7 +609,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();
 
@@ -1197,7 +1245,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(" = ");
@@ -1768,7 +1816,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) {
@@ -2179,7 +2226,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;
 		}
 	}
@@ -2328,9 +2375,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() {
@@ -2461,14 +2508,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;

+ 2 - 2
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();

+ 1 - 1
editor/plugins/script_editor_plugin.cpp

@@ -3317,7 +3317,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;

+ 271 - 0
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);
+}

+ 42 - 0
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

+ 11 - 166
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
 }
 

+ 1 - 9
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

+ 2 - 72
modules/gdscript/gdscript_compiler.cpp

@@ -2152,12 +2152,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);
 	}
@@ -2226,23 +2220,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>();
@@ -2386,9 +2363,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;
@@ -2401,7 +2375,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;
 
@@ -2410,12 +2383,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: {
@@ -2423,18 +2390,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: {
@@ -2447,11 +2402,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: {
@@ -2459,19 +2409,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: {
@@ -2519,9 +2456,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
 	}
 
@@ -2614,7 +2548,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();
@@ -2623,7 +2557,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) {
@@ -2723,10 +2657,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());
 }
 

+ 14 - 3
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) {

+ 1 - 1
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