From c1b450566a6684ae8d24118fdb351ae4a382ab11 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Sun, 2 Jul 2023 13:13:38 +0300 Subject: [PATCH] GDScript: Add `@deprecated` and `@experimental` doc comment tags --- core/doc_data.h | 44 +++++- editor/editor_help.cpp | 29 +++- modules/gdscript/editor/gdscript_docgen.cpp | 44 ++++-- modules/gdscript/gdscript_parser.cpp | 151 ++++++++++++-------- modules/gdscript/gdscript_parser.h | 42 ++++-- 5 files changed, 211 insertions(+), 99 deletions(-) diff --git a/core/doc_data.h b/core/doc_data.h index 0fe7414b989..b8c92a4b677 100644 --- a/core/doc_data.h +++ b/core/doc_data.h @@ -532,6 +532,42 @@ public: } }; + struct EnumDoc { + String description; + bool is_deprecated = false; + bool is_experimental = false; + static EnumDoc from_dict(const Dictionary &p_dict) { + EnumDoc doc; + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + if (p_dict.has("is_deprecated")) { + doc.is_deprecated = p_dict["is_deprecated"]; + } + + if (p_dict.has("is_experimental")) { + doc.is_experimental = p_dict["is_experimental"]; + } + + return doc; + } + static Dictionary to_dict(const EnumDoc &p_doc) { + Dictionary dict; + + if (!p_doc.description.is_empty()) { + dict["description"] = p_doc.description; + } + + dict["is_deprecated"] = p_doc.is_deprecated; + + dict["is_experimental"] = p_doc.is_experimental; + + return dict; + } + }; + struct ClassDoc { String name; String inherits; @@ -543,7 +579,7 @@ public: Vector operators; Vector signals; Vector constants; - HashMap enums; + HashMap enums; Vector properties; Vector annotations; Vector theme_properties; @@ -626,7 +662,7 @@ public: enums = p_dict["enums"]; } for (int i = 0; i < enums.size(); i++) { - doc.enums[enums.get_key_at_index(i)] = enums.get_value_at_index(i); + doc.enums[enums.get_key_at_index(i)] = EnumDoc::from_dict(enums.get_value_at_index(i)); } Array properties; @@ -740,8 +776,8 @@ public: if (!p_doc.enums.is_empty()) { Dictionary enums; - for (const KeyValue &E : p_doc.enums) { - enums[E.key] = E.value; + for (const KeyValue &E : p_doc.enums) { + enums[E.key] = EnumDoc::to_dict(E.value); } dict["enums"] = enums; } diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 5a3841e4ca7..eceb62f0b48 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -1037,6 +1037,7 @@ void EditorHelp::_update_doc() { if (cd.properties[i].is_deprecated) { DEPRECATED_DOC_TAG; } + if (cd.properties[i].is_experimental) { EXPERIMENTAL_DOC_TAG; } @@ -1281,6 +1282,7 @@ void EditorHelp::_update_doc() { if (cd.signals[i].is_deprecated) { DEPRECATED_DOC_TAG; } + if (cd.signals[i].is_experimental) { EXPERIMENTAL_DOC_TAG; } @@ -1341,6 +1343,7 @@ void EditorHelp::_update_doc() { enum_line[E.key] = class_desc->get_paragraph_count() - 2; _push_code_font(); + class_desc->push_color(theme_cache.title_color); if (E.value.size() && E.value[0].is_bitfield) { class_desc->add_text("flags "); @@ -1357,21 +1360,32 @@ void EditorHelp::_update_doc() { class_desc->push_color(theme_cache.headline_color); class_desc->add_text(e); class_desc->pop(); - _pop_code_font(); class_desc->push_color(theme_cache.symbol_color); class_desc->add_text(":"); class_desc->pop(); + if (cd.enums.has(e)) { + if (cd.enums[e].is_deprecated) { + DEPRECATED_DOC_TAG; + } + + if (cd.enums[e].is_experimental) { + EXPERIMENTAL_DOC_TAG; + } + } + + _pop_code_font(); + class_desc->add_newline(); class_desc->add_newline(); // Enum description. - if (e != "@unnamed_enums" && cd.enums.has(e) && !cd.enums[e].strip_edges().is_empty()) { + if (e != "@unnamed_enums" && cd.enums.has(e) && !cd.enums[e].description.strip_edges().is_empty()) { class_desc->push_color(theme_cache.text_color); _push_normal_font(); class_desc->push_indent(1); - _add_text(cd.enums[e]); + _add_text(cd.enums[e].description); class_desc->pop(); _pop_normal_font(); class_desc->pop(); @@ -1395,6 +1409,7 @@ void EditorHelp::_update_doc() { constant_line[enum_list[i].name] = class_desc->get_paragraph_count() - 2; _push_code_font(); + _add_bulletpoint(); class_desc->push_color(theme_cache.headline_color); _add_text(enum_list[i].name); @@ -1405,7 +1420,6 @@ void EditorHelp::_update_doc() { class_desc->push_color(theme_cache.value_color); _add_text(_fix_constant(enum_list[i].value)); class_desc->pop(); - _pop_code_font(); if (enum_list[i].is_deprecated) { DEPRECATED_DOC_TAG; @@ -1415,6 +1429,8 @@ void EditorHelp::_update_doc() { EXPERIMENTAL_DOC_TAG; } + _pop_code_font(); + class_desc->add_newline(); if (!enum_list[i].description.strip_edges().is_empty()) { @@ -1481,8 +1497,6 @@ void EditorHelp::_update_doc() { _add_text(_fix_constant(constants[i].value)); class_desc->pop(); - _pop_code_font(); - if (constants[i].is_deprecated) { DEPRECATED_DOC_TAG; } @@ -1491,6 +1505,8 @@ void EditorHelp::_update_doc() { EXPERIMENTAL_DOC_TAG; } + _pop_code_font(); + class_desc->add_newline(); if (!constants[i].description.strip_edges().is_empty()) { @@ -1670,6 +1686,7 @@ void EditorHelp::_update_doc() { if (cd.properties[i].is_deprecated) { DEPRECATED_DOC_TAG; } + if (cd.properties[i].is_experimental) { EXPERIMENTAL_DOC_TAG; } diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp index 0d8453738d7..26f326838ce 100644 --- a/modules/gdscript/editor/gdscript_docgen.cpp +++ b/modules/gdscript/editor/gdscript_docgen.cpp @@ -101,14 +101,16 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c doc.inherits = p_script->native->get_name(); } - doc.brief_description = p_class->doc_brief_description; - doc.description = p_class->doc_description; - for (const Pair &p : p_class->doc_tutorials) { + doc.brief_description = p_class->doc_data.brief; + doc.description = p_class->doc_data.description; + for (const Pair &p : p_class->doc_data.tutorials) { DocData::TutorialDoc td; td.title = p.first; td.link = p.second; doc.tutorials.append(td); } + doc.is_deprecated = p_class->doc_data.is_deprecated; + doc.is_experimental = p_class->doc_data.is_experimental; for (const GDP::ClassNode::Member &member : p_class->members) { switch (member.type) { @@ -130,7 +132,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c 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); + DocData::constant_doc_from_variant(const_doc, const_name, m_const->initializer->reduced_value, m_const->doc_data.description); + const_doc.is_deprecated = m_const->doc_data.is_deprecated; + const_doc.is_experimental = m_const->doc_data.is_experimental; doc.constants.push_back(const_doc); } break; @@ -153,7 +157,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c } DocData::MethodDoc method_doc; - DocData::method_doc_from_methodinfo(method_doc, mi, m_func->doc_description); + DocData::method_doc_from_methodinfo(method_doc, mi, m_func->doc_data.description); + method_doc.is_deprecated = m_func->doc_data.is_deprecated; + method_doc.is_experimental = m_func->doc_data.is_experimental; doc.methods.push_back(method_doc); } break; @@ -172,7 +178,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c } DocData::MethodDoc signal_doc; - DocData::signal_doc_from_methodinfo(signal_doc, mi, m_signal->doc_description); + DocData::signal_doc_from_methodinfo(signal_doc, mi, m_signal->doc_data.description); + signal_doc.is_deprecated = m_signal->doc_data.is_deprecated; + signal_doc.is_experimental = m_signal->doc_data.is_experimental; doc.signals.push_back(signal_doc); } break; @@ -185,7 +193,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c DocData::PropertyDoc prop_doc; prop_doc.name = var_name; - prop_doc.description = m_var->doc_description; + prop_doc.description = m_var->doc_data.description; + prop_doc.is_deprecated = m_var->doc_data.is_deprecated; + prop_doc.is_experimental = m_var->doc_data.is_experimental; GDType dt = m_var->get_datatype(); switch (dt.kind) { @@ -236,15 +246,21 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c p_script->member_lines[name] = m_enum->start_line; - doc.enums[name] = m_enum->doc_description; + DocData::EnumDoc enum_doc; + enum_doc.description = m_enum->doc_data.description; + enum_doc.is_deprecated = m_enum->doc_data.is_deprecated; + enum_doc.is_experimental = m_enum->doc_data.is_experimental; + doc.enums[name] = enum_doc; 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.is_value_valid = true; - const_doc.description = val.doc_description; const_doc.enumeration = name; + const_doc.description = val.doc_data.description; + const_doc.is_deprecated = val.doc_data.is_deprecated; + const_doc.is_experimental = val.doc_data.is_experimental; doc.constants.push_back(const_doc); } @@ -257,10 +273,12 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c 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); + DocData::ConstantDoc const_doc; + DocData::constant_doc_from_variant(const_doc, name, m_enum_val.value, m_enum_val.doc_data.description); + const_doc.enumeration = "@unnamed_enums"; + const_doc.is_deprecated = m_enum_val.doc_data.is_deprecated; + const_doc.is_experimental = m_enum_val.doc_data.is_experimental; + doc.constants.push_back(const_doc); } break; case GDP::ClassNode::Member::GROUP: case GDP::ClassNode::Member::UNDEFINED: diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index e17ee0668fd..b2a82a06f52 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -571,8 +571,8 @@ void GDScriptParser::parse_program() { class_doc_line = MIN(class_doc_line, E.key); } } - if (has_comment(class_doc_line)) { - get_class_doc_comment(class_doc_line, head->doc_brief_description, head->doc_description, head->doc_tutorials, false); + if (has_comment(class_doc_line, true)) { + head->doc_data = parse_class_doc_comment(class_doc_line, false); } #endif // TOOLS_ENABLED @@ -771,12 +771,16 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b // 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); + if constexpr (std::is_same_v) { + member->doc_data = parse_class_doc_comment(previous.start_line, true, true); + } else { + member->doc_data = parse_doc_comment(previous.start_line, true); + } } else if (has_comment(doc_comment_line, true)) { if constexpr (std::is_same_v) { - get_class_doc_comment(doc_comment_line, member->doc_brief_description, member->doc_description, member->doc_tutorials, true); + member->doc_data = parse_class_doc_comment(doc_comment_line, true); } else { - member->doc_description = get_doc_comment(doc_comment_line); + member->doc_data = parse_doc_comment(doc_comment_line); } } #endif // TOOLS_ENABLED @@ -1327,25 +1331,34 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { #ifdef TOOLS_ENABLED // Enum values documentation. for (int i = 0; i < enum_node->values.size(); i++) { + int doc_comment_line = enum_node->values[i].line; + bool single_line = false; + + if (has_comment(doc_comment_line, true)) { + single_line = true; + } else if (has_comment(doc_comment_line - 1, true)) { + doc_comment_line--; + } else { + continue; + } + if (i == enum_node->values.size() - 1) { // If close bracket is same line as last value. - if (enum_node->values[i].line != previous.start_line && has_comment(enum_node->values[i].line)) { - if (named) { - enum_node->values.write[i].doc_description = get_doc_comment(enum_node->values[i].line, true); - } else { - current_class->set_enum_value_doc(enum_node->values[i].identifier->name, get_doc_comment(enum_node->values[i].line, true)); - } + if (doc_comment_line == previous.start_line) { + break; } } else { // If two values are same line. - if (enum_node->values[i].line != enum_node->values[i + 1].line && has_comment(enum_node->values[i].line)) { - if (named) { - enum_node->values.write[i].doc_description = get_doc_comment(enum_node->values[i].line, true); - } else { - current_class->set_enum_value_doc(enum_node->values[i].identifier->name, get_doc_comment(enum_node->values[i].line, true)); - } + if (doc_comment_line == enum_node->values[i + 1].line) { + continue; } } + + if (named) { + enum_node->values.write[i].doc_data = parse_doc_comment(doc_comment_line, single_line); + } else { + current_class->set_enum_value_doc_data(enum_node->values[i].identifier->name, parse_doc_comment(doc_comment_line, single_line)); + } } #endif // TOOLS_ENABLED @@ -3423,19 +3436,20 @@ bool GDScriptParser::has_comment(int p_line, bool p_must_be_doc) { return tokenizer.get_comments()[p_line].comment.begins_with("##"); } -String GDScriptParser::get_doc_comment(int p_line, bool p_single_line) { +GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool p_single_line) { + MemberDocData result; + const HashMap &comments = tokenizer.get_comments(); - ERR_FAIL_COND_V(!comments.has(p_line), String()); + ERR_FAIL_COND_V(!comments.has(p_line), result); if (p_single_line) { if (comments[p_line].comment.begins_with("##")) { - return comments[p_line].comment.trim_prefix("##").strip_edges(); + result.description = comments[p_line].comment.trim_prefix("##").strip_edges(); + return result; } - return ""; + return result; } - String doc; - int line = p_line; DocLineState state = DOC_LINE_NORMAL; @@ -3463,29 +3477,42 @@ String GDScriptParser::get_doc_comment(int p_line, bool p_single_line) { } String doc_line = comments[line].comment.trim_prefix("##"); - doc += _process_doc_line(doc_line, doc, space_prefix, state); line++; + + if (state == DOC_LINE_NORMAL) { + String stripped_line = doc_line.strip_edges(); + if (stripped_line.begins_with("@deprecated")) { + result.is_deprecated = true; + continue; + } else if (stripped_line.begins_with("@experimental")) { + result.is_experimental = true; + continue; + } + } + + result.description += _process_doc_line(doc_line, result.description, space_prefix, state); } - return doc; + return result; } -void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String &p_desc, Vector> &p_tutorials, bool p_inner_class) { +GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, bool p_inner_class, bool p_single_line) { + ClassDocData result; + const HashMap &comments = tokenizer.get_comments(); - if (!comments.has(p_line)) { - return; + ERR_FAIL_COND_V(!comments.has(p_line), result); + + if (p_single_line) { + if (comments[p_line].comment.begins_with("##")) { + result.brief = comments[p_line].comment.trim_prefix("##").strip_edges(); + return result; + } + return result; } - ERR_FAIL_COND(!p_brief.is_empty() || !p_desc.is_empty() || p_tutorials.size() != 0); int line = p_line; DocLineState state = DOC_LINE_NORMAL; - enum Mode { - BRIEF, - DESC, - TUTORIALS, - DONE, - }; - Mode mode = BRIEF; + bool is_in_brief = true; if (p_inner_class) { while (comments.has(line - 1)) { @@ -3512,18 +3539,21 @@ void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String & break; } - String doc_line = comments[line++].comment.trim_prefix("##"); - String title, link; // For tutorials. + String doc_line = comments[line].comment.trim_prefix("##"); + line++; if (state == DOC_LINE_NORMAL) { - // Set the read mode. String stripped_line = doc_line.strip_edges(); - if (stripped_line.is_empty()) { - if (mode == BRIEF && !p_brief.is_empty()) { - mode = DESC; - } + + // A blank line separates the description from the brief. + if (is_in_brief && !result.brief.is_empty() && stripped_line.is_empty()) { + is_in_brief = false; continue; - } else if (stripped_line.begins_with("@tutorial")) { + } + + if (stripped_line.begins_with("@tutorial")) { + String title, link; + int begin_scan = String("@tutorial").length(); if (begin_scan >= stripped_line.length()) { continue; // Invalid syntax. @@ -3565,24 +3595,21 @@ void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String & link = stripped_line.substr(colon_pos).strip_edges(); } - mode = TUTORIALS; - } else if (mode == TUTORIALS) { // Tutorial docs are single line, we need a @tag after it. - mode = DONE; + result.tutorials.append(Pair(title, link)); + continue; + } else if (stripped_line.begins_with("@deprecated")) { + result.is_deprecated = true; + continue; + } else if (stripped_line.begins_with("@experimental")) { + result.is_experimental = true; + continue; } } - switch (mode) { - case BRIEF: - p_brief += _process_doc_line(doc_line, p_brief, space_prefix, state); - break; - case DESC: - p_desc += _process_doc_line(doc_line, p_desc, space_prefix, state); - break; - case TUTORIALS: - p_tutorials.append(Pair(title, link)); - break; - case DONE: - break; + if (is_in_brief) { + result.brief += _process_doc_line(doc_line, result.brief, space_prefix, state); + } else { + result.description += _process_doc_line(doc_line, result.description, space_prefix, state); } } @@ -3590,11 +3617,11 @@ void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String & const ClassNode::Member &m = current_class->members[0]; int first_member_line = m.get_line(); if (first_member_line == line) { - p_brief = ""; - p_desc = ""; - p_tutorials.clear(); + result = ClassDocData(); // Clear result. } } + + return result; } #endif // TOOLS_ENABLED diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 18757eb9fd2..517bdfdaf02 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -257,6 +257,22 @@ public: int line = 0, column = 0; }; +#ifdef TOOLS_ENABLED + struct ClassDocData { + String brief; + String description; + Vector> tutorials; + bool is_deprecated = false; + bool is_experimental = false; + }; + + struct MemberDocData { + String description; + bool is_deprecated = false; + bool is_experimental = false; + }; +#endif // TOOLS_ENABLED + struct Node { enum Type { NONE, @@ -505,7 +521,7 @@ public: int leftmost_column = 0; int rightmost_column = 0; #ifdef TOOLS_ENABLED - String doc_description; + MemberDocData doc_data; #endif // TOOLS_ENABLED }; @@ -513,7 +529,7 @@ public: Vector values; Variant dictionary; #ifdef TOOLS_ENABLED - String doc_description; + MemberDocData doc_data; #endif // TOOLS_ENABLED EnumNode() { @@ -720,14 +736,12 @@ public: DataType base_type; String fqcn; // Fully-qualified class name. Identifies uniquely any class in the project. #ifdef TOOLS_ENABLED - String doc_description; - String doc_brief_description; - Vector> doc_tutorials; + ClassDocData doc_data; // EnumValue docs are parsed after itself, so we need a method to add/modify the doc property later. - void set_enum_value_doc(const StringName &p_name, const String &p_doc_description) { + void set_enum_value_doc_data(const StringName &p_name, const MemberDocData &p_doc_data) { ERR_FAIL_INDEX(members_indices[p_name], members.size()); - members.write[members_indices[p_name]].enum_value.doc_description = p_doc_description; + members.write[members_indices[p_name]].enum_value.doc_data = p_doc_data; } #endif // TOOLS_ENABLED @@ -764,7 +778,7 @@ public: struct ConstantNode : public AssignableNode { #ifdef TOOLS_ENABLED - String doc_description; + MemberDocData doc_data; #endif // TOOLS_ENABLED ConstantNode() { @@ -819,7 +833,7 @@ public: LambdaNode *source_lambda = nullptr; #ifdef TOOLS_ENABLED Vector default_arg_values; - String doc_description; + MemberDocData doc_data; #endif // TOOLS_ENABLED bool resolved_signature = false; @@ -1006,7 +1020,7 @@ public: Vector parameters; HashMap parameters_indices; #ifdef TOOLS_ENABLED - String doc_description; + MemberDocData doc_data; #endif // TOOLS_ENABLED SignalNode() { @@ -1211,7 +1225,7 @@ public: int assignments = 0; bool is_static = false; #ifdef TOOLS_ENABLED - String doc_description; + MemberDocData doc_data; #endif // TOOLS_ENABLED VariableNode() { @@ -1486,12 +1500,12 @@ private: ExpressionNode *parse_yield(ExpressionNode *p_previous_operand, bool p_can_assign); ExpressionNode *parse_invalid_token(ExpressionNode *p_previous_operand, bool p_can_assign); TypeNode *parse_type(bool p_allow_void = false); + #ifdef TOOLS_ENABLED - // Doc comments. int class_doc_line = 0x7FFFFFFF; 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> &p_tutorials, bool p_inner_class); + MemberDocData parse_doc_comment(int p_line, bool p_single_line = false); + ClassDocData parse_class_doc_comment(int p_line, bool p_inner_class, bool p_single_line = false); #endif // TOOLS_ENABLED public: