diff options
Diffstat (limited to 'modules/gdscript/gdscript_parser.cpp')
-rw-r--r-- | modules/gdscript/gdscript_parser.cpp | 1034 |
1 files changed, 681 insertions, 353 deletions
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index c402b63f7b..db7b3e7ace 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -30,29 +30,36 @@ #include "gdscript_parser.h" +#include "gdscript.h" + #include "core/config/project_settings.h" #include "core/io/file_access.h" #include "core/io/resource_loader.h" #include "core/math/math_defs.h" -#include "gdscript.h" #include "scene/main/multiplayer_api.h" #ifdef DEBUG_ENABLED #include "core/os/os.h" #include "core/string/string_builder.h" -#include "gdscript_warning.h" #include "servers/text_server.h" -#endif // DEBUG_ENABLED +#endif #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" -#endif // TOOLS_ENABLED +#endif +// This function is used to determine that a type is "built-in" as opposed to native +// and custom classes. So `Variant::NIL` and `Variant::OBJECT` are excluded: +// `Variant::NIL` - `null` is literal, not a type. +// `Variant::OBJECT` - `Object` should be treated as a class, not as a built-in type. static HashMap<StringName, Variant::Type> builtin_types; Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) { - if (builtin_types.is_empty()) { - for (int i = 1; i < Variant::VARIANT_MAX; i++) { - builtin_types[Variant::get_type_name((Variant::Type)i)] = (Variant::Type)i; + if (unlikely(builtin_types.is_empty())) { + for (int i = 0; i < Variant::VARIANT_MAX; i++) { + Variant::Type type = (Variant::Type)i; + if (type != Variant::NIL && type != Variant::OBJECT) { + builtin_types[Variant::get_type_name(type)] = type; + } } } @@ -62,6 +69,10 @@ Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) { return Variant::VARIANT_MAX; } +#ifdef TOOLS_ENABLED +HashMap<String, String> GDScriptParser::theme_color_names; +#endif + void GDScriptParser::cleanup() { builtin_types.clear(); } @@ -81,6 +92,8 @@ GDScriptParser::GDScriptParser() { // TODO: Should this be static? register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation); register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation); + register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation); + register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); // Export annotations. register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>); @@ -102,6 +115,7 @@ GDScriptParser::GDScriptParser() { register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_RENDER, Variant::INT>); register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_PHYSICS, Variant::INT>); register_annotation(MethodInfo("@export_flags_3d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_NAVIGATION, Variant::INT>); + register_annotation(MethodInfo("@export_flags_avoidance"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_AVOIDANCE, Variant::INT>); // Export grouping annotations. register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>); register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray("")); @@ -109,11 +123,20 @@ GDScriptParser::GDScriptParser() { // Warning annotations. register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, varray(), true); // Networking. - register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::rpc_annotation, varray("authority", "call_remote", "unreliable", 0), true); + register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::rpc_annotation, varray("authority", "call_remote", "unreliable", 0)); #ifdef DEBUG_ENABLED is_ignoring_warnings = !(bool)GLOBAL_GET("debug/gdscript/warnings/enable"); #endif + +#ifdef TOOLS_ENABLED + if (theme_color_names.is_empty()) { + theme_color_names.insert("x", "axis_x_color"); + theme_color_names.insert("y", "axis_y_color"); + theme_color_names.insert("z", "axis_z_color"); + theme_color_names.insert("w", "axis_w_color"); + } +#endif } GDScriptParser::~GDScriptParser() { @@ -150,7 +173,7 @@ void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { #ifdef DEBUG_ENABLED void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Vector<String> &p_symbols) { - ERR_FAIL_COND(p_source == nullptr); + ERR_FAIL_NULL(p_source); if (is_ignoring_warnings) { return; } @@ -363,8 +386,10 @@ GDScriptTokenizer::Token GDScriptParser::advance() { push_error(current.literal); current = tokenizer.scan(); } - for (Node *n : nodes_in_progress) { - update_extents(n); + if (previous.type != GDScriptTokenizer::Token::DEDENT) { // `DEDENT` belongs to the next non-empty line. + for (Node *n : nodes_in_progress) { + update_extents(n); + } } return previous; } @@ -490,7 +515,7 @@ void GDScriptParser::parse_program() { if (annotation->applies_to(AnnotationInfo::SCRIPT)) { // `@icon` needs to be applied in the parser. See GH-72444. if (annotation->name == SNAME("@icon")) { - annotation->apply(this, head); + annotation->apply(this, head, nullptr); } else { head->annotations.push_back(annotation); } @@ -559,13 +584,14 @@ void GDScriptParser::parse_program() { complete_extents(head); #ifdef TOOLS_ENABLED - for (const KeyValue<int, GDScriptTokenizer::CommentData> &E : tokenizer.get_comments()) { - if (E.value.new_line && E.value.comment.begins_with("##")) { - class_doc_line = MIN(class_doc_line, E.key); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + int line = MIN(max_script_doc_line, head->end_line); + while (line > 0) { + if (comments.has(line) && comments[line].new_line && comments[line].comment.begins_with("##")) { + head->doc_data = parse_class_doc_comment(line); + break; } - } - if (has_comment(class_doc_line)) { - get_class_doc_comment(class_doc_line, head->doc_brief_description, head->doc_description, head->doc_tutorials, false); + line--; } #endif // TOOLS_ENABLED @@ -623,7 +649,7 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const { return false; } -GDScriptParser::ClassNode *GDScriptParser::parse_class() { +GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) { ClassNode *n_class = alloc_node<ClassNode>(); ClassNode *previous_class = current_class; @@ -712,25 +738,21 @@ void GDScriptParser::parse_extends() { if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected superclass name after "extends".)")) { return; } - current_class->extends.push_back(previous.literal); + current_class->extends.push_back(parse_identifier()); while (match(GDScriptTokenizer::Token::PERIOD)) { make_completion_context(COMPLETION_INHERIT_TYPE, current_class, chain_index++); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected superclass name after ".".)")) { return; } - current_class->extends.push_back(previous.literal); + current_class->extends.push_back(parse_identifier()); } } template <class T> -void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind) { +void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) { advance(); -#ifdef TOOLS_ENABLED - int doc_comment_line = previous.start_line - 1; -#endif // TOOLS_ENABLED - // Consume annotations. List<AnnotationNode *> annotations; while (!annotation_stack.is_empty()) { @@ -742,50 +764,51 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)() push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind)); clear_unused_annotations(); } -#ifdef TOOLS_ENABLED - if (last_annotation->start_line == doc_comment_line) { - doc_comment_line--; - } -#endif // TOOLS_ENABLED } - T *member = (this->*p_parse_function)(); + T *member = (this->*p_parse_function)(p_is_static); if (member == nullptr) { return; } +#ifdef TOOLS_ENABLED + int doc_comment_line = member->start_line - 1; +#endif // TOOLS_ENABLED + for (AnnotationNode *&annotation : annotations) { member->annotations.push_back(annotation); +#ifdef TOOLS_ENABLED + if (annotation->start_line <= doc_comment_line) { + doc_comment_line = annotation->start_line - 1; + } +#endif // TOOLS_ENABLED } #ifdef TOOLS_ENABLED - // Consume doc comments. - class_doc_line = MIN(class_doc_line, doc_comment_line - 1); - if (has_comment(doc_comment_line)) { - 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 { - member->doc_description = get_doc_comment(doc_comment_line); + if constexpr (std::is_same_v<T, ClassNode>) { + if (has_comment(member->start_line, true)) { + // Inline doc comment. + member->doc_data = parse_class_doc_comment(member->start_line, true); + } else if (has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + // Normal doc comment. Don't check `min_member_doc_line` because a class ends parsing after its members. + // This may not work correctly for cases like `var a; class B`, but it doesn't matter in practice. + member->doc_data = parse_class_doc_comment(doc_comment_line); + } + } else { + if (has_comment(member->start_line, true)) { + // Inline doc comment. + member->doc_data = parse_doc_comment(member->start_line, true); + } else if (doc_comment_line >= min_member_doc_line && has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + // Normal doc comment. + member->doc_data = parse_doc_comment(doc_comment_line); } } + + min_member_doc_line = member->end_line + 1; // Prevent multiple members from using the same doc comment. #endif // TOOLS_ENABLED if (member->identifier != nullptr) { if (!((String)member->identifier->name).is_empty()) { // Enums may be unnamed. - -#ifdef DEBUG_ENABLED - List<MethodInfo> gdscript_funcs; - GDScriptLanguage::get_singleton()->get_public_functions(&gdscript_funcs); - for (MethodInfo &info : gdscript_funcs) { - if (info.name == member->identifier->name) { - push_warning(member->identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, p_member_kind, member->identifier->name, "built-in function"); - } - } - if (Variant::has_utility_function(member->identifier->name)) { - push_warning(member->identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, p_member_kind, member->identifier->name, "built-in function"); - } -#endif - if (current_class->members_indices.has(member->identifier->name)) { push_error(vformat(R"(%s "%s" has the same name as a previously declared %s.)", p_member_kind.capitalize(), member->identifier->name, current_class->get_member(member->identifier->name).get_type_name()), member->identifier); } else { @@ -799,10 +822,15 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)() void GDScriptParser::parse_class_body(bool p_is_multiline) { bool class_end = false; + bool next_is_static = false; while (!class_end && !is_at_end()) { - switch (current.type) { + GDScriptTokenizer::Token token = current; + switch (token.type) { case GDScriptTokenizer::Token::VAR: - parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable"); + parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static); + if (next_is_static) { + current_class->has_static_data = true; + } break; case GDScriptTokenizer::Token::CONST: parse_class_member(&GDScriptParser::parse_constant, AnnotationInfo::CONSTANT, "constant"); @@ -810,9 +838,8 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::SIGNAL: parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal"); break; - case GDScriptTokenizer::Token::STATIC: case GDScriptTokenizer::Token::FUNC: - parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function"); + parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_static); break; case GDScriptTokenizer::Token::CLASS: parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class"); @@ -820,6 +847,13 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::ENUM: parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum"); break; + case GDScriptTokenizer::Token::STATIC: { + advance(); + next_is_static = true; + if (!check(GDScriptTokenizer::Token::FUNC) && !check(GDScriptTokenizer::Token::VAR)) { + push_error(R"(Expected "func" or "var" after "static".)"); + } + } break; case GDScriptTokenizer::Token::ANNOTATION: { advance(); @@ -866,6 +900,9 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { advance(); break; } + if (token.type != GDScriptTokenizer::Token::STATIC) { + next_is_static = false; + } if (panic_mode) { synchronize(); } @@ -875,11 +912,11 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { } } -GDScriptParser::VariableNode *GDScriptParser::parse_variable() { - return parse_variable(true); +GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) { + return parse_variable(p_is_static, true); } -GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_property) { +GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) { VariableNode *variable = alloc_node<VariableNode>(); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) { @@ -889,6 +926,7 @@ GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_proper variable->identifier = parse_identifier(); variable->export_info.name = variable->identifier->name; + variable->is_static = p_is_static; if (match(GDScriptTokenizer::Token::COLON)) { if (check(GDScriptTokenizer::Token::NEWLINE)) { @@ -1032,6 +1070,7 @@ void GDScriptParser::parse_property_setter(VariableNode *p_variable) { complete_extents(identifier); identifier->name = "@" + p_variable->identifier->name + "_setter"; function->identifier = identifier; + function->is_static = p_variable->is_static; consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after "set".)"); @@ -1083,6 +1122,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) { complete_extents(identifier); identifier->name = "@" + p_variable->identifier->name + "_getter"; function->identifier = identifier; + function->is_static = p_variable->is_static; FunctionNode *previous_function = current_function; current_function = function; @@ -1107,10 +1147,11 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) { } } -GDScriptParser::ConstantNode *GDScriptParser::parse_constant() { +GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) { ConstantNode *constant = alloc_node<ConstantNode>(); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) { + complete_extents(constant); return nullptr; } @@ -1174,7 +1215,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() { return parameter; } -GDScriptParser::SignalNode *GDScriptParser::parse_signal() { +GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) { SignalNode *signal = alloc_node<SignalNode>(); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) { @@ -1219,7 +1260,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal() { return signal; } -GDScriptParser::EnumNode *GDScriptParser::parse_enum() { +GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { EnumNode *enum_node = alloc_node<EnumNode>(); bool named = false; @@ -1231,6 +1272,9 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum() { push_multiline(true); consume(GDScriptTokenizer::Token::BRACE_OPEN, vformat(R"(Expected "{" after %s.)", named ? "enum name" : R"("enum")")); +#ifdef TOOLS_ENABLED + int min_enum_value_doc_line = previous.end_line + 1; +#endif HashMap<StringName, int> elements; @@ -1293,34 +1337,35 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum() { } } while (match(GDScriptTokenizer::Token::COMMA)); - pop_multiline(); - consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" for enum.)"); - #ifdef TOOLS_ENABLED // Enum values documentation. for (int i = 0; i < enum_node->values.size(); i++) { - 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)); - } + int enum_value_line = enum_node->values[i].line; + int doc_comment_line = enum_value_line - 1; + + MemberDocData doc_data; + if (has_comment(enum_value_line, true)) { + // Inline doc comment. + if (i == enum_node->values.size() - 1 || enum_node->values[i + 1].line > enum_value_line) { + doc_data = parse_doc_comment(enum_value_line, true); } + } else if (doc_comment_line >= min_enum_value_doc_line && has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + // Normal doc comment. + doc_data = parse_doc_comment(doc_comment_line); + } + + if (named) { + enum_node->values.write[i].doc_data = doc_data; } 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)); - } - } + current_class->set_enum_value_doc_data(enum_node->values[i].identifier->name, doc_data); } + + min_enum_value_doc_line = enum_value_line + 1; // Prevent multiple enum values from using the same doc comment. } #endif // TOOLS_ENABLED + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" for enum.)"); complete_extents(enum_node); end_statement("enum"); @@ -1343,7 +1388,7 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod default_used = true; } else { if (default_used) { - push_error("Cannot have a mandatory parameters after optional parameters."); + push_error("Cannot have mandatory parameters after optional parameters."); continue; } } @@ -1368,23 +1413,23 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod } } + if (!p_function->source_lambda && p_function->identifier && p_function->identifier->name == GDScriptLanguage::get_singleton()->strings._static_init) { + if (!p_function->is_static) { + push_error(R"(Static constructor must be declared static.)"); + } + if (p_function->parameters.size() != 0) { + push_error(R"(Static constructor cannot have parameters.)"); + } + current_class->has_static_data = true; + } + // TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens. consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type)); } -GDScriptParser::FunctionNode *GDScriptParser::parse_function() { +GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) { FunctionNode *function = alloc_node<FunctionNode>(); - bool _static = false; - if (previous.type == GDScriptTokenizer::Token::STATIC) { - // TODO: Improve message if user uses "static" with "var" or "const" - if (!consume(GDScriptTokenizer::Token::FUNC, R"(Expected "func" after "static".)")) { - complete_extents(function); - return nullptr; - } - _static = true; - } - make_completion_context(COMPLETION_OVERRIDE_METHOD, function); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after "func".)")) { @@ -1396,7 +1441,7 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function() { current_function = function; function->identifier = parse_identifier(); - function->is_static = _static; + function->is_static = p_is_static; SuiteNode *body = alloc_node<SuiteNode>(); SuiteNode *previous_suite = current_suite; @@ -1439,27 +1484,32 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali valid = false; } - if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + if (check(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + push_multiline(true); + advance(); // Arguments. push_completion_call(annotation); make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, 0, true); - if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { - push_multiline(true); - int argument_index = 0; - do { - make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index, true); - set_last_completion_call_arg(argument_index++); - ExpressionNode *argument = parse_expression(false); - if (argument == nullptr) { - valid = false; - continue; - } - annotation->arguments.push_back(argument); - } while (match(GDScriptTokenizer::Token::COMMA)); - pop_multiline(); + int argument_index = 0; + do { + if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Allow for trailing comma. + break; + } - consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after annotation arguments.)*"); - } + make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index, true); + set_last_completion_call_arg(argument_index++); + ExpressionNode *argument = parse_expression(false); + if (argument == nullptr) { + push_error("Expected expression as the annotation argument."); + valid = false; + continue; + } + annotation->arguments.push_back(argument); + } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end()); + + pop_multiline(); + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after annotation arguments.)*"); pop_completion_call(); } complete_extents(annotation); @@ -1475,7 +1525,7 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali void GDScriptParser::clear_unused_annotations() { for (const AnnotationNode *annotation : annotation_stack) { - push_error(vformat(R"(Annotation "%s" does not precedes a valid target, so it will have no effect.)", annotation->name), annotation); + push_error(vformat(R"(Annotation "%s" does not precede a valid target, so it will have no effect.)", annotation->name), annotation); } annotation_stack.clear(); @@ -1503,6 +1553,11 @@ GDScriptParser::SuiteNode *GDScriptParser::parse_suite(const String &p_context, suite->parent_function = current_function; current_suite = suite; + if (!p_for_lambda && suite->parent_block != nullptr && suite->parent_block->is_in_loop) { + // Do not reset to false if true is set before calling parse_suite(). + suite->is_in_loop = true; + } + bool multiline = false; if (match(GDScriptTokenizer::Token::NEWLINE)) { @@ -1521,7 +1576,7 @@ GDScriptParser::SuiteNode *GDScriptParser::parse_suite(const String &p_context, int error_count = 0; do { - if (!multiline && previous.type == GDScriptTokenizer::Token::SEMICOLON && check(GDScriptTokenizer::Token::NEWLINE)) { + if (is_at_end() || (!multiline && previous.type == GDScriptTokenizer::Token::SEMICOLON && check(GDScriptTokenizer::Token::NEWLINE))) { break; } Node *statement = parse_statement(); @@ -1603,11 +1658,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { break; case GDScriptTokenizer::Token::VAR: advance(); - result = parse_variable(); + result = parse_variable(false, false); break; case GDScriptTokenizer::Token::CONST: advance(); - result = parse_constant(); + result = parse_constant(false); break; case GDScriptTokenizer::Token::IF: advance(); @@ -1637,7 +1692,7 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { advance(); ReturnNode *n_return = alloc_node<ReturnNode>(); if (!is_statement_end()) { - if (current_function && current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init) { + if (current_function && (current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init || current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._static_init)) { push_error(R"(Constructor cannot return a value.)"); } n_return->return_value = parse_expression(false); @@ -1812,12 +1867,23 @@ GDScriptParser::ForNode *GDScriptParser::parse_for() { n_for->variable = parse_identifier(); } - consume(GDScriptTokenizer::Token::IN, R"(Expected "in" after "for" variable name.)"); + if (match(GDScriptTokenizer::Token::COLON)) { + n_for->datatype_specifier = parse_type(); + if (n_for->datatype_specifier == nullptr) { + push_error(R"(Expected type specifier after ":".)"); + } + } + + if (n_for->datatype_specifier == nullptr) { + consume(GDScriptTokenizer::Token::IN, R"(Expected "in" or ":" after "for" variable name.)"); + } else { + consume(GDScriptTokenizer::Token::IN, R"(Expected "in" after "for" variable type specifier.)"); + } n_for->list = parse_expression(false); if (!n_for->list) { - push_error(R"(Expected a list or range after "in".)"); + push_error(R"(Expected iterable after "in".)"); } consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "for" condition.)"); @@ -1838,9 +1904,8 @@ GDScriptParser::ForNode *GDScriptParser::parse_for() { } suite->add_local(SuiteNode::Local(n_for->variable, current_function)); } - + suite->is_in_loop = true; n_for->loop = parse_suite(R"("for" block)", suite); - n_for->loop->is_loop = true; complete_extents(n_for); // Reset break/continue state. @@ -1973,7 +2038,37 @@ GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() { push_error(R"(No pattern found for "match" branch.)"); } - if (!consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "match" patterns.)")) { + bool has_guard = false; + if (match(GDScriptTokenizer::Token::WHEN)) { + // Pattern guard. + // Create block for guard because it also needs to access the bound variables from patterns, and we don't want to add them to the outer scope. + branch->guard_body = alloc_node<SuiteNode>(); + if (branch->patterns.size() > 0) { + for (const KeyValue<StringName, IdentifierNode *> &E : branch->patterns[0]->binds) { + SuiteNode::Local local(E.value, current_function); + local.type = SuiteNode::Local::PATTERN_BIND; + branch->guard_body->add_local(local); + } + } + + SuiteNode *parent_block = current_suite; + branch->guard_body->parent_block = parent_block; + current_suite = branch->guard_body; + + ExpressionNode *guard = parse_expression(false); + if (guard == nullptr) { + push_error(R"(Expected expression for pattern guard after "when".)"); + } else { + branch->guard_body->statements.append(guard); + } + current_suite = parent_block; + complete_extents(branch->guard_body); + + has_guard = true; + branch->has_wildcard = false; // If it has a guard, the wildcard might still not match. + } + + if (!consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":"%s after "match" %s.)", has_guard ? "" : R"( or "when")", has_guard ? "pattern guard" : "patterns"))) { complete_extents(branch); return nullptr; } @@ -2110,6 +2205,7 @@ GDScriptParser::PatternNode *GDScriptParser::parse_match_pattern(PatternNode *p_ ExpressionNode *expression = parse_expression(false); if (expression == nullptr) { push_error(R"(Expected expression for match pattern.)"); + complete_extents(pattern); return nullptr; } else { if (expression->type == GDScriptParser::Node::LITERAL) { @@ -2153,8 +2249,9 @@ GDScriptParser::WhileNode *GDScriptParser::parse_while() { can_break = true; can_continue = true; - n_while->loop = parse_suite(R"("while" block)"); - n_while->loop->is_loop = true; + SuiteNode *suite = alloc_node<SuiteNode>(); + suite->is_in_loop = true; + n_while->loop = parse_suite(R"("while" block)", suite); complete_extents(n_while); // Reset break/continue state. @@ -2198,7 +2295,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr ExpressionNode *previous_operand = (this->*prefix_rule)(nullptr, p_can_assign); while (p_precedence <= get_rule(current.type)->precedence) { - if (previous_operand == nullptr || (p_stop_on_assign && current.type == GDScriptTokenizer::Token::EQUAL) || (previous_operand->type == Node::LAMBDA && lambda_ended)) { + if (previous_operand == nullptr || (p_stop_on_assign && current.type == GDScriptTokenizer::Token::EQUAL) || lambda_ended) { return previous_operand; } // Also switch multiline mode on here for infix operators. @@ -2241,6 +2338,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_identifier(ExpressionNode IdentifierNode *identifier = alloc_node<IdentifierNode>(); complete_extents(identifier); identifier->name = previous.get_identifier(); + identifier->suite = current_suite; if (current_suite != nullptr && current_suite->has_local(identifier->name)) { const SuiteNode::Local &declaration = current_suite->get_local(identifier->name); @@ -2404,7 +2502,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_binary_operator(Expression complete_extents(operation); if (operation->right_operand == nullptr) { - push_error(vformat(R"(Expected expression after "%s" operator.")", op.get_name())); + push_error(vformat(R"(Expected expression after "%s" operator.)", op.get_name())); } // TODO: Also for unary, ternary, and assignment. @@ -2731,12 +2829,12 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_dictionary(ExpressionNode switch (dictionary->style) { case DictionaryNode::LUA_TABLE: if (key != nullptr && key->type != Node::IDENTIFIER && key->type != Node::LITERAL) { - push_error("Expected identifier or string as LUA-style dictionary key."); + push_error(R"(Expected identifier or string as Lua-style dictionary key (e.g "{ key = value }").)"); advance(); break; } if (key != nullptr && key->type == Node::LITERAL && static_cast<LiteralNode *>(key)->value.get_type() != Variant::STRING) { - push_error("Expected identifier or string as LUA-style dictionary key."); + push_error(R"(Expected identifier or string as Lua-style dictionary key (e.g "{ key = value }").)"); advance(); break; } @@ -2820,6 +2918,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_attribute(ExpressionNode * attribute->base = p_previous_operand; + if (current.is_node_name()) { + current.type = GDScriptTokenizer::Token::IDENTIFIER; + } if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier after "." for attribute access.)")) { complete_extents(attribute); return attribute; @@ -2990,10 +3091,8 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p if (previous.type == GDScriptTokenizer::Token::DOLLAR) { // Detect initial slash, which will be handled in the loop if it matches. match(GDScriptTokenizer::Token::SLASH); -#ifdef DEBUG_ENABLED } else { get_node->use_dollar = false; -#endif } int context_argument = 0; @@ -3097,6 +3196,8 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_preload(ExpressionNode *p_ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_previous_operand, bool p_can_assign) { LambdaNode *lambda = alloc_node<LambdaNode>(); lambda->parent_function = current_function; + lambda->parent_lambda = current_lambda; + FunctionNode *function = alloc_node<FunctionNode>(); function->source_lambda = lambda; @@ -3124,6 +3225,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p FunctionNode *previous_function = current_function; current_function = function; + LambdaNode *previous_lambda = current_lambda; + current_lambda = lambda; + SuiteNode *body = alloc_node<SuiteNode>(); body->parent_function = current_function; body->parent_block = current_suite; @@ -3161,6 +3265,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p } current_function = previous_function; + current_lambda = previous_lambda; in_lambda = previous_in_lambda; lambda->function = function; @@ -3188,7 +3293,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_type_test(ExpressionNode * } GDScriptParser::ExpressionNode *GDScriptParser::parse_yield(ExpressionNode *p_previous_operand, bool p_can_assign) { - push_error(R"("yield" was removed in Godot 4.0. Use "await" instead.)"); + push_error(R"("yield" was removed in Godot 4. Use "await" instead.)"); return nullptr; } @@ -3261,222 +3366,281 @@ GDScriptParser::TypeNode *GDScriptParser::parse_type(bool p_allow_void) { } #ifdef TOOLS_ENABLED -static bool _in_codeblock(String p_line, bool p_already_in, int *r_block_begins = nullptr) { - int start_block = p_line.rfind("[codeblock]"); - int end_block = p_line.rfind("[/codeblock]"); - - if (start_block != -1 && r_block_begins) { - *r_block_begins = start_block; +enum DocLineState { + DOC_LINE_NORMAL, + DOC_LINE_IN_CODE, + DOC_LINE_IN_CODEBLOCK, +}; + +static String _process_doc_line(const String &p_line, const String &p_text, const String &p_space_prefix, DocLineState &r_state) { + String line = p_line; + if (r_state == DOC_LINE_NORMAL) { + line = line.strip_edges(true, false); + } else { + line = line.trim_prefix(p_space_prefix); } - if (p_already_in) { - if (end_block == -1) { - return true; - } else if (start_block == -1) { - return false; + String line_join; + if (!p_text.is_empty()) { + if (r_state == DOC_LINE_NORMAL) { + if (p_text.ends_with("[/codeblock]")) { + line_join = "\n"; + } else if (!p_text.ends_with("[br]")) { + line_join = " "; + } } else { - return start_block > end_block; + line_join = "\n"; } - } else { - if (start_block == -1) { - return false; - } else if (end_block == -1) { - return true; - } else { - return start_block > end_block; + } + + String result; + int from = 0; + int buffer_start = 0; + const int len = line.length(); + bool process = true; + while (process) { + switch (r_state) { + case DOC_LINE_NORMAL: { + int lb_pos = line.find_char('[', from); + if (lb_pos < 0) { + process = false; + break; + } + int rb_pos = line.find_char(']', lb_pos + 1); + if (rb_pos < 0) { + process = false; + break; + } + + from = rb_pos + 1; + + String tag = line.substr(lb_pos + 1, rb_pos - lb_pos - 1); + if (tag == "code") { + r_state = DOC_LINE_IN_CODE; + } else if (tag == "codeblock") { + if (lb_pos == 0) { + line_join = "\n"; + } else { + result += line.substr(buffer_start, lb_pos - buffer_start) + '\n'; + } + result += "[codeblock]"; + if (from < len) { + result += '\n'; + } + + r_state = DOC_LINE_IN_CODEBLOCK; + buffer_start = from; + } + } break; + case DOC_LINE_IN_CODE: { + int pos = line.find("[/code]", from); + if (pos < 0) { + process = false; + break; + } + + from = pos + 7; + + r_state = DOC_LINE_NORMAL; + } break; + case DOC_LINE_IN_CODEBLOCK: { + int pos = line.find("[/codeblock]", from); + if (pos < 0) { + process = false; + break; + } + + from = pos + 12; + + if (pos == 0) { + line_join = "\n"; + } else { + result += line.substr(buffer_start, pos - buffer_start) + '\n'; + } + result += "[/codeblock]"; + if (from < len) { + result += '\n'; + } + + r_state = DOC_LINE_NORMAL; + buffer_start = from; + } break; } } + + result += line.substr(buffer_start); + if (r_state == DOC_LINE_NORMAL) { + result = result.strip_edges(false, true); + } + + return line_join + result; } -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) { +GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool p_single_line) { + ERR_FAIL_COND_V(!has_comment(p_line, true), MemberDocData()); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); - ERR_FAIL_COND_V(!comments.has(p_line), String()); + int line = p_line; - if (p_single_line) { - if (comments[p_line].comment.begins_with("##")) { - return comments[p_line].comment.trim_prefix("##").strip_edges(); + if (!p_single_line) { + while (comments.has(line - 1) && comments[line - 1].new_line && comments[line - 1].comment.begins_with("##")) { + line--; } - return ""; } - String doc; + max_script_doc_line = MIN(max_script_doc_line, line - 1); - int line = p_line; - bool in_codeblock = false; - - while (comments.has(line - 1)) { - if (!comments[line - 1].new_line || !comments[line - 1].comment.begins_with("##")) { - break; + String space_prefix; + { + int i = 2; + for (; i < comments[line].comment.length(); i++) { + if (comments[line].comment[i] != ' ') { + break; + } } - line--; + space_prefix = String(" ").repeat(i - 2); } - int codeblock_begins = 0; - while (comments.has(line)) { - if (!comments[line].new_line || !comments[line].comment.begins_with("##")) { - break; - } - String doc_line = comments[line].comment.trim_prefix("##"); + DocLineState state = DOC_LINE_NORMAL; + MemberDocData result; - in_codeblock = _in_codeblock(doc_line, in_codeblock, &codeblock_begins); + while (line <= p_line) { + String doc_line = comments[line].comment.trim_prefix("##"); + line++; - if (in_codeblock) { - int i = 0; - for (; i < codeblock_begins; i++) { - if (doc_line[i] != ' ') { - break; - } + 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; } - doc_line = doc_line.substr(i); - } else { - doc_line = doc_line.strip_edges(); } - String line_join = (in_codeblock) ? "\n" : " "; - doc = (doc.is_empty()) ? doc_line : doc + line_join + doc_line; - line++; + 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<Pair<String, String>> &p_tutorials, bool p_inner_class) { +GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, bool p_single_line) { + ERR_FAIL_COND_V(!has_comment(p_line, true), ClassDocData()); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); - if (!comments.has(p_line)) { - return; + int line = p_line; + + if (!p_single_line) { + while (comments.has(line - 1) && comments[line - 1].new_line && comments[line - 1].comment.begins_with("##")) { + line--; + } } - ERR_FAIL_COND(!p_brief.is_empty() || !p_desc.is_empty() || p_tutorials.size() != 0); - int line = p_line; - bool in_codeblock = false; - enum Mode { - BRIEF, - DESC, - TUTORIALS, - DONE, - }; - Mode mode = BRIEF; + max_script_doc_line = MIN(max_script_doc_line, line - 1); - if (p_inner_class) { - while (comments.has(line - 1)) { - if (!comments[line - 1].new_line || !comments[line - 1].comment.begins_with("##")) { + String space_prefix; + { + int i = 2; + for (; i < comments[line].comment.length(); i++) { + if (comments[line].comment[i] != ' ') { break; } - line--; } + space_prefix = String(" ").repeat(i - 2); } - int codeblock_begins = 0; - while (comments.has(line)) { - if (!comments[line].new_line || !comments[line].comment.begins_with("##")) { - break; - } + DocLineState state = DOC_LINE_NORMAL; + bool is_in_brief = true; + ClassDocData result; - String title, link; // For tutorials. - String doc_line = comments[line++].comment.trim_prefix("##"); - String stripped_line = doc_line.strip_edges(); + while (line <= p_line) { + String doc_line = comments[line].comment.trim_prefix("##"); + line++; - // Set the read mode. - if (stripped_line.is_empty() && mode == BRIEF && !p_brief.is_empty()) { - mode = DESC; - continue; + if (state == DOC_LINE_NORMAL) { + String stripped_line = doc_line.strip_edges(); - } else if (stripped_line.begins_with("@tutorial")) { - int begin_scan = String("@tutorial").length(); - if (begin_scan >= stripped_line.length()) { - continue; // invalid syntax. + // 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; } - if (stripped_line[begin_scan] == ':') { // No title. - // Syntax: ## @tutorial: https://godotengine.org/ // The title argument is optional. - title = ""; - link = stripped_line.trim_prefix("@tutorial:").strip_edges(); - - } else { - /* Syntax: - * @tutorial ( The Title Here ) : https://the.url/ - * ^ open ^ close ^ colon ^ url - */ - int open_bracket_pos = begin_scan, close_bracket_pos = 0; - while (open_bracket_pos < stripped_line.length() && (stripped_line[open_bracket_pos] == ' ' || stripped_line[open_bracket_pos] == '\t')) { - open_bracket_pos++; - } - if (open_bracket_pos == stripped_line.length() || stripped_line[open_bracket_pos++] != '(') { - continue; // invalid syntax. - } - close_bracket_pos = open_bracket_pos; - while (close_bracket_pos < stripped_line.length() && stripped_line[close_bracket_pos] != ')') { - close_bracket_pos++; - } - if (close_bracket_pos == stripped_line.length()) { - continue; // invalid syntax. - } + if (stripped_line.begins_with("@tutorial")) { + String title, link; - int colon_pos = close_bracket_pos + 1; - while (colon_pos < stripped_line.length() && (stripped_line[colon_pos] == ' ' || stripped_line[colon_pos] == '\t')) { - colon_pos++; - } - if (colon_pos == stripped_line.length() || stripped_line[colon_pos++] != ':') { - continue; // invalid syntax. + int begin_scan = String("@tutorial").length(); + if (begin_scan >= stripped_line.length()) { + continue; // Invalid syntax. } - title = stripped_line.substr(open_bracket_pos, close_bracket_pos - open_bracket_pos).strip_edges(); - link = stripped_line.substr(colon_pos).strip_edges(); - } - - mode = TUTORIALS; - in_codeblock = false; - } else if (stripped_line.is_empty()) { - continue; - } else { - // Tutorial docs are single line, we need a @tag after it. - if (mode == TUTORIALS) { - mode = DONE; - } + if (stripped_line[begin_scan] == ':') { // No title. + // Syntax: ## @tutorial: https://godotengine.org/ // The title argument is optional. + title = ""; + link = stripped_line.trim_prefix("@tutorial:").strip_edges(); + } else { + /* Syntax: + * @tutorial ( The Title Here ) : https://the.url/ + * ^ open ^ close ^ colon ^ url + */ + int open_bracket_pos = begin_scan, close_bracket_pos = 0; + while (open_bracket_pos < stripped_line.length() && (stripped_line[open_bracket_pos] == ' ' || stripped_line[open_bracket_pos] == '\t')) { + open_bracket_pos++; + } + if (open_bracket_pos == stripped_line.length() || stripped_line[open_bracket_pos++] != '(') { + continue; // Invalid syntax. + } + close_bracket_pos = open_bracket_pos; + while (close_bracket_pos < stripped_line.length() && stripped_line[close_bracket_pos] != ')') { + close_bracket_pos++; + } + if (close_bracket_pos == stripped_line.length()) { + continue; // Invalid syntax. + } - in_codeblock = _in_codeblock(doc_line, in_codeblock, &codeblock_begins); - } + int colon_pos = close_bracket_pos + 1; + while (colon_pos < stripped_line.length() && (stripped_line[colon_pos] == ' ' || stripped_line[colon_pos] == '\t')) { + colon_pos++; + } + if (colon_pos == stripped_line.length() || stripped_line[colon_pos++] != ':') { + continue; // Invalid syntax. + } - if (in_codeblock) { - int i = 0; - for (; i < codeblock_begins; i++) { - if (doc_line[i] != ' ') { - break; + title = stripped_line.substr(open_bracket_pos, close_bracket_pos - open_bracket_pos).strip_edges(); + link = stripped_line.substr(colon_pos).strip_edges(); } + + result.tutorials.append(Pair<String, String>(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; } - doc_line = doc_line.substr(i); - } else { - doc_line = stripped_line; } - String line_join = (in_codeblock) ? "\n" : " "; - switch (mode) { - case BRIEF: - p_brief = (p_brief.length() == 0) ? doc_line : p_brief + line_join + doc_line; - break; - case DESC: - p_desc = (p_desc.length() == 0) ? doc_line : p_desc + line_join + doc_line; - break; - case TUTORIALS: - p_tutorials.append(Pair<String, String>(title, link)); - break; - case DONE: - break; - } - } - if (current_class->members.size() > 0) { - 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(); + 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); } } + + return result; } #endif // TOOLS_ENABLED @@ -3543,6 +3707,7 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty { nullptr, nullptr, PREC_NONE }, // PASS, { nullptr, nullptr, PREC_NONE }, // RETURN, { nullptr, nullptr, PREC_NONE }, // MATCH, + { nullptr, nullptr, PREC_NONE }, // WHEN, // Keywords { nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS, { nullptr, nullptr, PREC_NONE }, // ASSERT, @@ -3626,12 +3791,12 @@ const GDScriptParser::SuiteNode::Local &GDScriptParser::SuiteNode::get_local(con return empty; } -bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) { +bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target, ClassNode *p_class) { if (is_applied) { return true; } is_applied = true; - return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target); + return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target, p_class); } bool GDScriptParser::AnnotationNode::applies_to(uint32_t p_target_kinds) const { @@ -3677,29 +3842,62 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) return true; } -bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_node) { +bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +#ifdef DEBUG_ENABLED + if (this->_is_tool) { + push_error(R"("@tool" annotation can only be used once.)", p_annotation); + return false; + } +#endif // DEBUG_ENABLED this->_is_tool = true; return true; } -bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p_node) { - ERR_FAIL_COND_V_MSG(p_node->type != Node::CLASS, false, R"("@icon" annotation can only be applied to classes.)"); +bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, R"("@icon" annotation can only be applied to classes.)"); ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false); - ClassNode *p_class = static_cast<ClassNode *>(p_node); - p_class->icon_path = p_annotation->resolved_arguments[0]; + + ClassNode *class_node = static_cast<ClassNode *>(p_target); + String path = p_annotation->resolved_arguments[0]; + +#ifdef DEBUG_ENABLED + if (!class_node->icon_path.is_empty()) { + push_error(R"("@icon" annotation can only be used once.)", p_annotation); + return false; + } + if (path.is_empty()) { + push_error(R"("@icon" annotation argument must contain the path to the icon.)", p_annotation->arguments[0]); + return false; + } +#endif // DEBUG_ENABLED + + class_node->icon_path = path; + + if (path.is_empty() || path.is_absolute_path()) { + class_node->simplified_icon_path = path.simplify_path(); + } else if (path.is_relative_path()) { + class_node->simplified_icon_path = script_path.get_base_dir().path_join(path).simplify_path(); + } else { + class_node->simplified_icon_path = path; + } + return true; } -bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node *p_node) { - ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)"); +bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)"); if (current_class && !ClassDB::is_parent_class(current_class->get_datatype().native_type, SNAME("Node"))) { push_error(R"("@onready" can only be used in classes that inherit "Node".)", p_annotation); } - VariableNode *variable = static_cast<VariableNode *>(p_node); + VariableNode *variable = static_cast<VariableNode *>(p_target); + if (variable->is_static) { + push_error(R"("@onready" annotation cannot be applied to a static variable.)", p_annotation); + return false; + } if (variable->onready) { - push_error(R"("@onready" annotation can only be used once per variable.)"); + push_error(R"("@onready" annotation can only be used once per variable.)", p_annotation); return false; } variable->onready = true; @@ -3708,10 +3906,15 @@ bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node } template <PropertyHint t_hint, Variant::Type t_type> -bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_node) { - ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); +bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); + ERR_FAIL_NULL_V(p_class, false); - VariableNode *variable = static_cast<VariableNode *>(p_node); + VariableNode *variable = static_cast<VariableNode *>(p_target); + if (variable->is_static) { + push_error(vformat(R"(Annotation "%s" cannot be applied to a static variable.)", p_annotation->name), p_annotation); + return false; + } if (variable->exported) { push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation); return false; @@ -3737,6 +3940,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } + // WARNING: Do not merge with the previous `if` because there `!=`, not `==`! if (p_annotation->name == SNAME("@export_flags")) { const int64_t max_flags = 32; Vector<String> t = arg_string.split(":", true, 1); @@ -3762,6 +3966,18 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Starting from argument %d, the flag value must be specified explicitly.)", i + 1, max_flags + 1), p_annotation->arguments[i]); return false; } + } else if (p_annotation->name == SNAME("@export_node_path")) { + String native_class = arg_string; + if (ScriptServer::is_global_class(arg_string)) { + native_class = ScriptServer::get_global_class_native_base(arg_string); + } + if (!ClassDB::class_exists(native_class)) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_node_path": The class "%s" was not found in the global scope.)", i + 1, arg_string), p_annotation->arguments[i]); + return false; + } else if (!ClassDB::is_parent_class(native_class, SNAME("Node"))) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_node_path": The class "%s" does not inherit "Node".)", i + 1, arg_string), p_annotation->arguments[i]); + return false; + } } if (i > 0) { @@ -3779,8 +3995,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node if (export_type.builtin_type == Variant::INT) { variable->export_info.type = Variant::INT; } - } - if (p_annotation->name == SNAME("@export_multiline")) { + } else if (p_annotation->name == SNAME("@export_multiline")) { if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type()) { DataType inner_type = export_type.get_container_element_type(); if (inner_type.builtin_type != Variant::STRING) { @@ -3808,6 +4023,8 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } + // WARNING: Do not merge with the previous `else if`! Otherwise `else` (default variable type check) + // will not work for the above annotations. `@export` and `@export_enum` validate the type separately. if (p_annotation->name == SNAME("@export")) { if (variable->datatype_specifier == nullptr && variable->initializer == nullptr) { push_error(R"(Cannot use simple "@export" annotation with variable without type or initializer, since type can't be inferred.)", p_annotation); @@ -3856,7 +4073,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node variable->export_info.hint = PROPERTY_HINT_NODE_TYPE; variable->export_info.hint_string = export_type.to_string(); } else { - push_error(R"(Export type can only be built-in, a resource, a node or an enum.)", variable); + push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable); return false; } @@ -3882,28 +4099,38 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } break; case GDScriptParser::DataType::ENUM: { - variable->export_info.type = Variant::INT; - variable->export_info.hint = PROPERTY_HINT_ENUM; - - String enum_hint_string; - bool first = true; - for (const KeyValue<StringName, int64_t> &E : export_type.enum_values) { - if (!first) { - enum_hint_string += ","; - } else { - first = false; + if (export_type.is_meta_type) { + variable->export_info.type = Variant::DICTIONARY; + } else { + variable->export_info.type = Variant::INT; + variable->export_info.hint = PROPERTY_HINT_ENUM; + + String enum_hint_string; + bool first = true; + for (const KeyValue<StringName, int64_t> &E : export_type.enum_values) { + if (!first) { + enum_hint_string += ","; + } else { + first = false; + } + enum_hint_string += E.key.operator String().capitalize().xml_escape(); + enum_hint_string += ":"; + enum_hint_string += String::num_int64(E.value).xml_escape(); } - enum_hint_string += E.key.operator String().capitalize().xml_escape(); - enum_hint_string += ":"; - enum_hint_string += String::num_int64(E.value).xml_escape(); - } - variable->export_info.hint_string = enum_hint_string; + variable->export_info.hint_string = enum_hint_string; + variable->export_info.usage |= PROPERTY_USAGE_CLASS_IS_ENUM; + variable->export_info.class_name = String(export_type.native_type).replace("::", "."); + } } break; default: - // TODO: Allow custom user resources. - push_error(R"(Export type can only be built-in, a resource, or an enum.)", variable); - break; + push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable); + return false; + } + + if (variable->export_info.hint == PROPERTY_HINT_NODE_TYPE && !ClassDB::is_parent_class(p_class->base_type.native_type, SNAME("Node"))) { + push_error(vformat(R"(Node export is only supported in Node-derived classes, but the current class inherits "%s".)", p_class->base_type.to_string()), variable); + return false; } if (is_array) { @@ -3948,7 +4175,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } template <PropertyUsageFlags t_usage> -bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation, Node *p_node) { +bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { AnnotationNode *annotation = const_cast<AnnotationNode *>(p_annotation); if (annotation->resolved_arguments.is_empty()) { @@ -3980,7 +4207,7 @@ bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation return true; } -bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_node) { +bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { #ifdef DEBUG_ENABLED bool has_error = false; for (const Variant &warning_name : p_annotation->resolved_arguments) { @@ -3989,7 +4216,7 @@ bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Nod push_error(vformat(R"(Invalid warning name: "%s".)", warning_name), p_annotation); has_error = true; } else { - p_node->ignored_warnings.push_back(warning); + p_target->ignored_warnings.push_back(warning); } } @@ -4001,10 +4228,10 @@ bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Nod #endif // DEBUG_ENABLED } -bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_node) { - ERR_FAIL_COND_V_MSG(p_node->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name)); +bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name)); - FunctionNode *function = static_cast<FunctionNode *>(p_node); + FunctionNode *function = static_cast<FunctionNode *>(p_target); if (function->rpc_config.get_type() != Variant::NIL) { push_error(R"(RPC annotations can only be used once per function.)", p_annotation); return false; @@ -4013,21 +4240,16 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_ Dictionary rpc_config; rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY; if (!p_annotation->resolved_arguments.is_empty()) { - int last = p_annotation->resolved_arguments.size() - 1; - if (p_annotation->resolved_arguments[last].get_type() == Variant::INT) { - rpc_config["channel"] = p_annotation->resolved_arguments[last].operator int(); - last -= 1; - } - if (last > 3) { - push_error(R"(Invalid RPC arguments. At most 4 arguments are allowed, where only the last argument can be an integer to specify the channel.')", p_annotation); - return false; - } - unsigned char locality_args = 0; unsigned char permission_args = 0; unsigned char transfer_mode_args = 0; - for (int i = last; i >= 0; i--) { + for (int i = 0; i < p_annotation->resolved_arguments.size(); i++) { + if (i == 3) { + rpc_config["channel"] = p_annotation->resolved_arguments[i].operator int(); + continue; + } + String arg = p_annotation->resolved_arguments[i].operator String(); if (arg == "call_local") { locality_args++; @@ -4067,6 +4289,17 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_ return true; } +bool GDScriptParser::static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, vformat(R"("%s" annotation can only be applied to classes.)", p_annotation->name)); + ClassNode *class_node = static_cast<ClassNode *>(p_target); + if (class_node->annotated_static_unload) { + push_error(vformat(R"("%s" annotation can only be used once per script.)", p_annotation->name), p_annotation); + return false; + } + class_node->annotated_static_unload = true; + return true; +} + GDScriptParser::DataType GDScriptParser::SuiteNode::Local::get_datatype() const { switch (type) { case CONSTANT: @@ -4095,7 +4328,7 @@ String GDScriptParser::SuiteNode::Local::get_name() const { case SuiteNode::Local::FOR_VARIABLE: return "for loop iterator"; case SuiteNode::Local::PATTERN_BIND: - return "pattern_bind"; + return "pattern bind"; case SuiteNode::Local::UNDEFINED: return "<undefined>"; default: @@ -4121,16 +4354,13 @@ String GDScriptParser::DataType::to_string() const { } return native_type.operator String(); case CLASS: - if (is_meta_type) { - return GDScript::get_class_static(); - } if (class_type->identifier != nullptr) { return class_type->identifier->name.operator String(); } return class_type->fqcn; case SCRIPT: { if (is_meta_type) { - return script_type->get_class_name().operator String(); + return script_type != nullptr ? script_type->get_class_name().operator String() : ""; } String name = script_type != nullptr ? script_type->get_name() : ""; if (!name.is_empty()) { @@ -4155,6 +4385,104 @@ String GDScriptParser::DataType::to_string() const { ERR_FAIL_V_MSG("<unresolved type>", "Kind set outside the enum range."); } +PropertyInfo GDScriptParser::DataType::to_property_info(const String &p_name) const { + PropertyInfo result; + result.name = p_name; + result.usage = PROPERTY_USAGE_NONE; + + if (!is_hard_type()) { + result.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + return result; + } + + switch (kind) { + case BUILTIN: + result.type = builtin_type; + if (builtin_type == Variant::ARRAY && has_container_element_type()) { + const DataType *elem_type = container_element_type; + switch (elem_type->kind) { + case BUILTIN: + result.hint = PROPERTY_HINT_ARRAY_TYPE; + result.hint_string = Variant::get_type_name(elem_type->builtin_type); + break; + case NATIVE: + result.hint = PROPERTY_HINT_ARRAY_TYPE; + result.hint_string = elem_type->native_type; + break; + case SCRIPT: + result.hint = PROPERTY_HINT_ARRAY_TYPE; + if (elem_type->script_type.is_valid() && elem_type->script_type->get_global_name() != StringName()) { + result.hint_string = elem_type->script_type->get_global_name(); + } else { + result.hint_string = elem_type->native_type; + } + break; + case CLASS: + result.hint = PROPERTY_HINT_ARRAY_TYPE; + if (elem_type->class_type != nullptr && elem_type->class_type->get_global_name() != StringName()) { + result.hint_string = elem_type->class_type->get_global_name(); + } else { + result.hint_string = elem_type->native_type; + } + break; + case ENUM: + result.hint = PROPERTY_HINT_ARRAY_TYPE; + result.hint_string = String(elem_type->native_type).replace("::", "."); + break; + case VARIANT: + case RESOLVING: + case UNRESOLVED: + break; + } + } + break; + case NATIVE: + result.type = Variant::OBJECT; + if (is_meta_type) { + result.class_name = GDScriptNativeClass::get_class_static(); + } else { + result.class_name = native_type; + } + break; + case SCRIPT: + result.type = Variant::OBJECT; + if (is_meta_type) { + result.class_name = script_type.is_valid() ? script_type->get_class() : Script::get_class_static(); + } else if (script_type.is_valid() && script_type->get_global_name() != StringName()) { + result.class_name = script_type->get_global_name(); + } else { + result.class_name = native_type; + } + break; + case CLASS: + result.type = Variant::OBJECT; + if (is_meta_type) { + result.class_name = GDScript::get_class_static(); + } else if (class_type != nullptr && class_type->get_global_name() != StringName()) { + result.class_name = class_type->get_global_name(); + } else { + result.class_name = native_type; + } + break; + case ENUM: + if (is_meta_type) { + result.type = Variant::DICTIONARY; + } else { + result.type = Variant::INT; + result.usage |= PROPERTY_USAGE_CLASS_IS_ENUM; + result.class_name = String(native_type).replace("::", "."); + } + break; + case VARIANT: + case RESOLVING: + case UNRESOLVED: + result.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + break; + } + + return result; +} + static Variant::Type _variant_type_to_typed_array_element_type(Variant::Type p_type) { switch (p_type) { case Variant::PACKED_BYTE_ARRAY: @@ -4479,7 +4807,7 @@ void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) { } else { first = false; } - push_text(p_class->extends[i]); + push_text(p_class->extends[i]->name); } } @@ -5078,7 +5406,7 @@ void GDScriptParser::TreePrinter::print_while(WhileNode *p_while) { } void GDScriptParser::TreePrinter::print_tree(const GDScriptParser &p_parser) { - ERR_FAIL_COND_MSG(p_parser.get_tree() == nullptr, "Parse the code before printing the parse tree."); + ERR_FAIL_NULL_MSG(p_parser.get_tree(), "Parse the code before printing the parse tree."); if (p_parser.is_tool()) { push_line("@tool"); |