diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index a9051a22d56..234dfd2b512 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -296,6 +296,27 @@ + + + + Marks a class or a method as abstract. + An abstract class is a class that cannot be instantiated directly. Instead, it is meant to be inherited by other classes. Attempting to instantiate an abstract class will result in an error. + An abstract method is a method that has no implementation. Therefore, a newline or a semicolon is expected after the function header. This defines a contract that inheriting classes must conform to, because the method signature must be compatible when overriding. + Inheriting classes must either provide implementations for all abstract methods, or the inheriting class must be marked as abstract. If a class has at least one abstract method (either its own or an unimplemented inherited one), then it must also be marked as abstract. However, the reverse is not true: an abstract class is allowed to have no abstract methods. + [codeblock] + @abstract class Shape: + @abstract func draw() + + class Circle extends Shape: + func draw(): + print("Drawing a circle.") + + class Square extends Shape: + func draw(): + print("Drawing a square.") + [/codeblock] + + diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index a0d8913b631..864efc67b95 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -2749,7 +2749,6 @@ Vector GDScriptLanguage::get_reserved_words() const { "when", "while", // Declarations. - "abstract", "class", "class_name", "const", @@ -2915,7 +2914,7 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b *r_icon_path = c->simplified_icon_path; } if (r_is_abstract) { - *r_is_abstract = false; + *r_is_abstract = c->is_abstract; } if (r_is_tool) { *r_is_tool = parser.is_tool(); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 093ee8bef4e..5f0791477ec 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -1540,12 +1540,12 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co if (member.function->is_abstract) { if (base_class == p_class) { const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name); - push_error(vformat(R"*(Class "%s" is not abstract but contains abstract methods. Mark the class as abstract or remove "abstract" from all methods in this class.)*", class_name), p_class); + push_error(vformat(R"*(Class "%s" is not abstract but contains abstract methods. Mark the class as "@abstract" or remove "@abstract" from all methods in this class.)*", class_name), p_class); break; } else if (!implemented_funcs.has(member.function->identifier->name)) { const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name); const String base_class_name = base_class->identifier == nullptr ? base_class->fqcn.get_file() : String(base_class->identifier->name); - push_error(vformat(R"*(Class "%s" must implement "%s.%s()" and other inherited abstract methods or be marked as abstract.)*", class_name, base_class_name, member.function->identifier->name), p_class); + push_error(vformat(R"*(Class "%s" must implement "%s.%s()" and other inherited abstract methods or be marked as "@abstract".)*", class_name, base_class_name, member.function->identifier->name), p_class); break; } } else { @@ -1987,6 +1987,20 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } p_function->resolved_body = true; + if (p_function->is_abstract) { + // Abstract functions don't have a body. + if (!p_function->body->statements.is_empty()) { + push_error(R"(Abstract function cannot have a body.)", p_function->body); + } + return; + } else { + // Non-abstract functions must have a body. + if (p_function->body->statements.is_empty()) { + push_error(R"(A function must either have a ":" followed by a body, or be marked as "@abstract".)", p_function); + return; + } + } + GDScriptParser::FunctionNode *previous_function = parser->current_function; parser->current_function = p_function; @@ -1999,7 +2013,7 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun // Use the suite inferred type if return isn't explicitly set. p_function->set_datatype(p_function->body->get_datatype()); } else if (p_function->get_datatype().is_hard_type() && (p_function->get_datatype().kind != GDScriptParser::DataType::BUILTIN || p_function->get_datatype().builtin_type != Variant::NIL)) { - if (!p_function->is_abstract && !p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) { + if (!p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) { push_error(R"(Not all code paths return a value.)", p_function); } } diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 7a54af84d86..07c4b26bea8 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1532,7 +1532,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context static const char *_keywords_with_space[] = { "and", "not", "or", "in", "as", "class", "class_name", "extends", "is", "func", "signal", "await", - "const", "enum", "abstract", "static", "var", "if", "elif", "else", "for", "match", "when", "while", + "const", "enum", "static", "var", "if", "elif", "else", "for", "match", "when", "while", nullptr }; diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 6cc79abea0c..79ca8513c32 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -96,6 +96,7 @@ GDScriptParser::GDScriptParser() { 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("@abstract"), AnnotationInfo::SCRIPT | AnnotationInfo::CLASS | AnnotationInfo::FUNCTION, &GDScriptParser::abstract_annotation); // Onready annotation. register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); // Export annotations. @@ -628,7 +629,7 @@ void GDScriptParser::parse_program() { annotation_stack.push_back(annotation); } else if (annotation->applies_to(AnnotationInfo::SCRIPT)) { PUSH_PENDING_ANNOTATIONS_TO_HEAD; - if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon")) { + if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon") || annotation->name == SNAME("@static_unload")) { // Some annotations need to be resolved and applied in the parser. // The root class is not in any class, so `head->outer == nullptr`. annotation->apply(this, head, nullptr); @@ -675,52 +676,9 @@ void GDScriptParser::parse_program() { reset_extents(head, current); } - bool first_is_abstract = false; while (can_have_class_or_extends) { // Order here doesn't matter, but there should be only one of each at most. switch (current.type) { - case GDScriptTokenizer::Token::ABSTRACT: { - if (head->is_abstract) { - // The root class is already marked as abstract, so this is - // the beginning of an abstract function or inner class. - can_have_class_or_extends = false; - break; - } - - const GDScriptTokenizer::Token abstract_token = current; - advance(); - - // A standalone "abstract" is only allowed for script-level stuff. - bool is_standalone = false; - if (current.type == GDScriptTokenizer::Token::NEWLINE) { - is_standalone = true; - end_statement("standalone \"abstract\""); - } - - switch (current.type) { - case GDScriptTokenizer::Token::CLASS_NAME: - case GDScriptTokenizer::Token::EXTENDS: - PUSH_PENDING_ANNOTATIONS_TO_HEAD; - head->is_abstract = true; - if (head->start_line == 1) { - reset_extents(head, abstract_token); - } - break; - case GDScriptTokenizer::Token::CLASS: - case GDScriptTokenizer::Token::FUNC: - if (is_standalone) { - push_error(R"(Expected "class_name" or "extends" after a standalone "abstract".)"); - } else { - first_is_abstract = true; - } - // This is the beginning of an abstract function or inner class. - can_have_class_or_extends = false; - break; - default: - push_error(R"(Expected "class_name", "extends", "class", or "func" after "abstract".)"); - break; - } - } break; case GDScriptTokenizer::Token::CLASS_NAME: PUSH_PENDING_ANNOTATIONS_TO_HEAD; advance(); @@ -765,15 +723,23 @@ void GDScriptParser::parse_program() { } } - // When the only thing needed is the class name and the icon, we don't need to parse the hole file. - // It really speed up the call to GDScriptLanguage::get_global_class_name especially for large script. +#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD + + for (AnnotationNode *&annotation : head->annotations) { + if (annotation->name == SNAME("@abstract")) { + // Some annotations need to be resolved and applied in the parser. + // The root class is not in any class, so `head->outer == nullptr`. + annotation->apply(this, head, nullptr); + } + } + + // When the only thing needed is the class name, icon, and abstractness; we don't need to parse the whole file. + // It really speed up the call to `GDScriptLanguage::get_global_class_name()` especially for large script. if (!parse_body) { return; } -#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD - - parse_class_body(first_is_abstract, true); + parse_class_body(true); head->end_line = current.end_line; head->end_column = current.end_column; @@ -876,13 +842,12 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const { return false; } -GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, bool p_is_static) { +GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) { ClassNode *n_class = alloc_node(); ClassNode *previous_class = current_class; current_class = n_class; n_class->outer = previous_class; - n_class->is_abstract = p_is_abstract; if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the class name after "class".)")) { n_class->identifier = parse_identifier(); @@ -919,7 +884,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, bool end_statement("superclass"); } - parse_class_body(false, multiline); + parse_class_body(multiline); complete_extents(n_class); if (multiline) { @@ -978,7 +943,7 @@ void GDScriptParser::parse_extends() { } template -void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool, bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_abstract, bool p_is_static) { +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(); // Consume annotations. @@ -994,7 +959,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b } } - T *member = (this->*p_parse_function)(p_is_abstract, p_is_static); + T *member = (this->*p_parse_function)(p_is_static); if (member == nullptr) { return; } @@ -1048,23 +1013,14 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b } } -void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multiline) { +void GDScriptParser::parse_class_body(bool p_is_multiline) { bool class_end = false; - // The header parsing code could consume `abstract` for the first function or inner class. - bool next_is_abstract = p_first_is_abstract; bool next_is_static = false; while (!class_end && !is_at_end()) { GDScriptTokenizer::Token token = current; switch (token.type) { - case GDScriptTokenizer::Token::ABSTRACT: { - advance(); - next_is_abstract = true; - if (!check(GDScriptTokenizer::Token::CLASS) && !check(GDScriptTokenizer::Token::FUNC)) { - push_error(R"(Expected "class" or "func" after "abstract".)"); - } - } break; case GDScriptTokenizer::Token::VAR: - parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", false, next_is_static); + parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static); if (next_is_static) { current_class->has_static_data = true; } @@ -1076,10 +1032,10 @@ void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multil parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal"); break; case GDScriptTokenizer::Token::FUNC: - parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_abstract, next_is_static); + 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", next_is_abstract); + parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class"); break; case GDScriptTokenizer::Token::ENUM: parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum"); @@ -1159,9 +1115,6 @@ void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multil } break; } - if (token.type != GDScriptTokenizer::Token::ABSTRACT) { - next_is_abstract = false; - } if (token.type != GDScriptTokenizer::Token::STATIC) { next_is_static = false; } @@ -1174,11 +1127,11 @@ void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multil } } -GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_abstract, bool p_is_static) { - return parse_variable(p_is_abstract, p_is_static, true); +GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) { + return parse_variable(p_is_static, true); } -GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_abstract, bool p_is_static, bool p_allow_property) { +GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) { VariableNode *variable = alloc_node(); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) { @@ -1414,7 +1367,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) { } } -GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_abstract, bool p_is_static) { +GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) { ConstantNode *constant = alloc_node(); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) { @@ -1482,7 +1435,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() { return parameter; } -GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_abstract, bool p_is_static) { +GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) { SignalNode *signal = alloc_node(); if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) { @@ -1527,7 +1480,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_abstract, boo return signal; } -GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_is_static) { +GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { EnumNode *enum_node = alloc_node(); bool named = false; @@ -1625,7 +1578,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_ return enum_node; } -void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start) { +bool GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start) { if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { bool default_used = false; do { @@ -1704,17 +1657,16 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod } #endif // TOOLS_ENABLED - if (p_function->is_abstract) { - end_statement("abstract function declaration"); - } else { - // 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)); + // TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens. + if (p_type == "lambda") { + return consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after lambda declaration.)"); } + // The colon may not be present in the case of abstract functions. + return match(GDScriptTokenizer::Token::COLON); } -GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) { +GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) { FunctionNode *function = alloc_node(); - function->is_abstract = p_is_abstract; function->is_static = p_is_static; make_completion_context(COMPLETION_OVERRIDE_METHOD, function); @@ -1744,9 +1696,9 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)"); #ifdef TOOLS_ENABLED - parse_function_signature(function, body, "function", signature_start_pos); + const bool has_body = parse_function_signature(function, body, "function", signature_start_pos); #else // !TOOLS_ENABLED - parse_function_signature(function, body, "function", -1); + const bool has_body = parse_function_signature(function, body, "function", -1); #endif // TOOLS_ENABLED current_suite = previous_suite; @@ -1755,7 +1707,9 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, function->min_local_doc_line = previous.end_line + 1; #endif // TOOLS_ENABLED - if (function->is_abstract) { + if (!has_body) { + // Abstract functions do not have a body. + end_statement("bodyless function declaration"); reset_extents(body, current); complete_extents(body); function->body = body; @@ -1995,11 +1949,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { break; case GDScriptTokenizer::Token::VAR: advance(); - result = parse_variable(false, false, false); + result = parse_variable(false, false); break; case GDScriptTokenizer::Token::TK_CONST: advance(); - result = parse_constant(false, false); + result = parse_constant(false); break; case GDScriptTokenizer::Token::IF: advance(); @@ -4220,7 +4174,6 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty { nullptr, nullptr, PREC_NONE }, // MATCH, { nullptr, nullptr, PREC_NONE }, // WHEN, // Keywords - { nullptr, nullptr, PREC_NONE }, // ABSTRACT { nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS, { nullptr, nullptr, PREC_NONE }, // ASSERT, { &GDScriptParser::parse_await, nullptr, PREC_NONE }, // AWAIT, @@ -4410,6 +4363,33 @@ bool GDScriptParser::static_unload_annotation(AnnotationNode *p_annotation, Node return true; } +bool GDScriptParser::abstract_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + // NOTE: Use `p_target`, **not** `p_class`, because when `p_target` is a class then `p_class` refers to the outer class. + if (p_target->type == Node::CLASS) { + ClassNode *class_node = static_cast(p_target); + if (class_node->is_abstract) { + push_error(R"("@abstract" annotation can only be used once per class.)", p_annotation); + return false; + } + class_node->is_abstract = true; + return true; + } + if (p_target->type == Node::FUNCTION) { + FunctionNode *function_node = static_cast(p_target); + if (function_node->is_static) { + push_error(R"("@abstract" annotation cannot be applied to static functions.)", p_annotation); + return false; + } + if (function_node->is_abstract) { + push_error(R"("@abstract" annotation can only be used once per function.)", p_annotation); + return false; + } + function_node->is_abstract = true; + return true; + } + ERR_FAIL_V_MSG(false, R"("@abstract" annotation can only be applied to classes and functions.)"); +} + bool GDScriptParser::onready_annotation(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.)"); @@ -5782,8 +5762,8 @@ void GDScriptParser::TreePrinter::print_cast(CastNode *p_cast) { } void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) { - if (p_class->is_abstract) { - push_text("Abstract "); + for (const AnnotationNode *E : p_class->annotations) { + print_annotation(E); } push_text("Class "); if (p_class->identifier == nullptr) { @@ -5984,9 +5964,6 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const for (const AnnotationNode *E : p_function->annotations) { print_annotation(E); } - if (p_function->is_abstract) { - push_text("Abstract "); - } if (p_function->is_static) { push_text("Static "); } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index b59ac7e5460..95735bc4ff8 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1503,17 +1503,17 @@ private: // Main blocks. void parse_program(); - ClassNode *parse_class(bool p_is_abstract, bool p_is_static); + ClassNode *parse_class(bool p_is_static); void parse_class_name(); void parse_extends(); - void parse_class_body(bool p_first_is_abstract, bool p_is_multiline); + void parse_class_body(bool p_is_multiline); template - void parse_class_member(T *(GDScriptParser::*p_parse_function)(bool, bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_abstract = false, bool p_is_static = false); - SignalNode *parse_signal(bool p_is_abstract, bool p_is_static); - EnumNode *parse_enum(bool p_is_abstract, bool p_is_static); + void parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static = false); + SignalNode *parse_signal(bool p_is_static); + EnumNode *parse_enum(bool p_is_static); ParameterNode *parse_parameter(); - FunctionNode *parse_function(bool p_is_abstract, bool p_is_static); - void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start); + FunctionNode *parse_function(bool p_is_static); + bool parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start); SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false); // Annotations AnnotationNode *parse_annotation(uint32_t p_valid_targets); @@ -1523,6 +1523,7 @@ private: bool tool_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); bool icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); bool static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool abstract_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); template bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); @@ -1536,12 +1537,12 @@ private: bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); // Statements. Node *parse_statement(); - VariableNode *parse_variable(bool p_is_abstract, bool p_is_static); - VariableNode *parse_variable(bool p_is_abstract, bool p_is_static, bool p_allow_property); + VariableNode *parse_variable(bool p_is_static); + VariableNode *parse_variable(bool p_is_static, bool p_allow_property); VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent); void parse_property_getter(VariableNode *p_variable); void parse_property_setter(VariableNode *p_variable); - ConstantNode *parse_constant(bool p_is_abstract, bool p_is_static); + ConstantNode *parse_constant(bool p_is_static); AssertNode *parse_assert(); BreakNode *parse_break(); ContinueNode *parse_continue(); diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index ea0e9d5cef2..c46555a9db6 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -101,7 +101,6 @@ static const char *token_names[] = { "match", // MATCH, "when", // WHEN, // Keywords - "abstract", // ABSTRACT, "as", // AS, "assert", // ASSERT, "await", // AWAIT, @@ -200,7 +199,6 @@ bool GDScriptTokenizer::Token::is_identifier() const { case IDENTIFIER: case MATCH: // Used in String.match(). case WHEN: // New keyword, avoid breaking existing code. - case ABSTRACT: // Allow constants to be treated as regular identifiers. case CONST_PI: case CONST_INF: @@ -216,7 +214,6 @@ bool GDScriptTokenizer::Token::is_node_name() const { // This is meant to allow keywords with the $ notation, but not as general identifiers. switch (type) { case IDENTIFIER: - case ABSTRACT: case AND: case AS: case ASSERT: @@ -491,7 +488,6 @@ GDScriptTokenizer::Token GDScriptTokenizerText::annotation() { #define KEYWORDS(KEYWORD_GROUP, KEYWORD) \ KEYWORD_GROUP('a') \ - KEYWORD("abstract", Token::ABSTRACT) \ KEYWORD("as", Token::AS) \ KEYWORD("and", Token::AND) \ KEYWORD("assert", Token::ASSERT) \ diff --git a/modules/gdscript/gdscript_tokenizer.h b/modules/gdscript/gdscript_tokenizer.h index 84e017c0e4b..f57cd4cd678 100644 --- a/modules/gdscript/gdscript_tokenizer.h +++ b/modules/gdscript/gdscript_tokenizer.h @@ -106,7 +106,6 @@ public: MATCH, WHEN, // Keywords - ABSTRACT, AS, ASSERT, AWAIT, diff --git a/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd index 9e4e788f00f..e50cf6d27f8 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd +++ b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd @@ -1,15 +1,15 @@ -abstract class AbstractClass: - abstract func some_func() +@abstract class AbstractClass: + @abstract func some_func() class ImplementedClass extends AbstractClass: func some_func(): pass -abstract class AbstractClassAgain extends ImplementedClass: - abstract func some_func() +@abstract class AbstractClassAgain extends ImplementedClass: + @abstract func some_func() class Test1: - abstract func some_func() + @abstract func some_func() class Test2 extends AbstractClass: pass @@ -24,5 +24,18 @@ class Test4 extends AbstractClass: func other_func(): super.some_func() +@abstract class A: + @abstract @abstract func abstract_dup() + + # An abstract function cannot have a body. + @abstract func abstract_bodyful(): + pass + + # A static function cannot be marked as `@abstract`. + @abstract static func abstract_stat() + +@abstract @abstract class DuplicateAbstract: + pass + func test(): pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out index 38dda90bf5d..4145012d087 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out @@ -1,6 +1,11 @@ GDTEST_ANALYZER_ERROR ->> ERROR at line 11: Class "Test1" is not abstract but contains abstract methods. Mark the class as abstract or remove "abstract" from all methods in this class. ->> ERROR at line 14: Class "Test2" must implement "AbstractClass.some_func()" and other inherited abstract methods or be marked as abstract. ->> ERROR at line 17: Class "Test3" must implement "AbstractClassAgain.some_func()" and other inherited abstract methods or be marked as abstract. +>> ERROR at line 37: "@abstract" annotation can only be used once per class. +>> ERROR at line 28: "@abstract" annotation can only be used once per function. +>> ERROR at line 35: "@abstract" annotation cannot be applied to static functions. +>> ERROR at line 11: Class "Test1" is not abstract but contains abstract methods. Mark the class as "@abstract" or remove "@abstract" from all methods in this class. +>> ERROR at line 14: Class "Test2" must implement "AbstractClass.some_func()" and other inherited abstract methods or be marked as "@abstract". +>> ERROR at line 17: Class "Test3" must implement "AbstractClassAgain.some_func()" and other inherited abstract methods or be marked as "@abstract". >> ERROR at line 22: Cannot call the parent class' abstract function "some_func()" because it hasn't been defined. >> ERROR at line 25: Cannot call the parent class' abstract function "some_func()" because it hasn't been defined. +>> ERROR at line 32: Abstract function cannot have a body. +>> ERROR at line 35: A function must either have a ":" followed by a body, or be marked as "@abstract". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_class.gd b/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_class.gd index 394e728510c..44cd8a493ea 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_class.gd +++ b/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_class.gd @@ -2,7 +2,7 @@ extends RefCounted const AbstractScript = preload("./construct_abstract_script.notest.gd") -abstract class AbstractClass: +@abstract class AbstractClass: pass func test(): diff --git a/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_script.notest.gd b/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_script.notest.gd index feac3d8251e..991bff85c03 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_script.notest.gd +++ b/modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_script.notest.gd @@ -1 +1 @@ -abstract class_name AbstractScript +@abstract class_name AbstractScript diff --git a/modules/gdscript/tests/scripts/analyzer/features/extend_abstract_class.gd b/modules/gdscript/tests/scripts/analyzer/features/extend_abstract_class.gd index 0a6e0378a16..a34262893a8 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/extend_abstract_class.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/extend_abstract_class.gd @@ -8,7 +8,7 @@ class B extends A: class C extends CanvasItem: pass -abstract class X: +@abstract class X: pass class Y extends X: diff --git a/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd b/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd index 09772812c5b..6443ffa7760 100644 --- a/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd +++ b/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd @@ -1,5 +1,5 @@ -abstract class A: - abstract func test(x: int) -> void +@abstract class A: + @abstract func test(x: int) -> void class B extends A: func ➡ diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd b/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd deleted file mode 100644 index 318f33f38e9..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends RefCounted - -abstract class A: - abstract func f(): - pass - -func test(): - pass diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out b/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out deleted file mode 100644 index a3669b49122..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_PARSER_ERROR -Expected end of statement after abstract function declaration, found ":" instead. diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd b/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd deleted file mode 100644 index 227adde2e14..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends RefCounted - -abstract class A: - # Currently, an abstract function cannot be static. - abstract static func f() - -func test(): - pass diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out b/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out deleted file mode 100644 index 1f481a5ed38..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_PARSER_ERROR -Expected "class" or "func" after "abstract". diff --git a/modules/gdscript/tests/scripts/parser/errors/brace_syntax.out b/modules/gdscript/tests/scripts/parser/errors/brace_syntax.out index 2f37a740ab9..16fd67eae45 100644 --- a/modules/gdscript/tests/scripts/parser/errors/brace_syntax.out +++ b/modules/gdscript/tests/scripts/parser/errors/brace_syntax.out @@ -1,2 +1,2 @@ GDTEST_PARSER_ERROR -Expected ":" after function declaration. +Expected end of statement after bodyless function declaration, found "{" instead. diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd deleted file mode 100644 index 3bca7d4a0da..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd +++ /dev/null @@ -1,7 +0,0 @@ -extends RefCounted - -abstract abstract class A: - pass - -func test(): - pass diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out deleted file mode 100644 index 220acdd43ed..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_PARSER_ERROR -Expected "class_name", "extends", "class", or "func" after "abstract". diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd deleted file mode 100644 index 23eeb079fe6..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd +++ /dev/null @@ -1,7 +0,0 @@ -extends RefCounted - -abstract class A: - abstract abstract func f() - -func test(): - pass diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out deleted file mode 100644 index 1f481a5ed38..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_PARSER_ERROR -Expected "class" or "func" after "abstract". diff --git a/modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.gd b/modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.gd new file mode 100644 index 00000000000..bf3fc32e9e6 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.gd @@ -0,0 +1,3 @@ +# No colon --v +var f = func () -> void + pass diff --git a/modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.out b/modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.out new file mode 100644 index 00000000000..f2f8b0592d0 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Expected ":" after lambda declaration. diff --git a/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd b/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd deleted file mode 100644 index baa652b57ba..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends RefCounted - -abstract class A: - # Currently, an abstract function cannot be static. - static abstract func f() - -func test(): - pass diff --git a/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out b/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out deleted file mode 100644 index ff3edc4d6f5..00000000000 --- a/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_PARSER_ERROR -Expected "func" or "var" after "static". diff --git a/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd b/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd index 2198d24549f..f2c9b860b60 100644 --- a/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd +++ b/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd @@ -1,18 +1,18 @@ -abstract class A: - abstract func get_text_1() -> String - abstract func get_text_2() -> String +@abstract class A: + @abstract func get_text_1() -> String + @abstract func get_text_2() -> String # No `UNUSED_PARAMETER` warning. - abstract func func_with_param(param: int) -> int - abstract func func_with_rest_param(...args: Array) -> int - abstract func func_with_semicolon() -> int; - abstract func func_1() -> int; abstract func func_2() -> int - abstract func func_without_return_type() + @abstract func func_with_param(param: int) -> int + @abstract func func_with_rest_param(...args: Array) -> int + @abstract func func_with_semicolon() -> int; + @abstract func func_1() -> int; @abstract func func_2() -> int + @abstract func func_without_return_type() func print_text_1() -> void: print(get_text_1()) -abstract class B extends A: +@abstract class B extends A: func get_text_1() -> String: return "text_1b" @@ -30,8 +30,8 @@ class C extends B: func func_2() -> int: return 0 func func_without_return_type(): pass -abstract class D extends C: - abstract func get_text_1() -> String +@abstract class D extends C: + @abstract func get_text_1() -> String func get_text_2() -> String: return super() + " text_2d" diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd index 40197650c3e..fe60d755937 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd @@ -2,9 +2,9 @@ @warning_ignore_start("unused_signal") -abstract class A: - abstract func test_abstract_func_1() - abstract func test_abstract_func_2() +@abstract class A: + @abstract func test_abstract_func_1() + @abstract func test_abstract_func_2() func test_override_func_1(): pass func test_override_func_2(): pass diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out index 33b30642788..663e74568cf 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out +++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out @@ -32,8 +32,8 @@ func test_override_func_1() -> void func test_override_func_2() -> void func test_func_b1() -> void func test_func_b2() -> void -abstract func test_abstract_func_1() -> void -abstract func test_abstract_func_2() -> void +@abstract func test_abstract_func_1() -> void +@abstract func test_abstract_func_2() -> void func test_override_func_1() -> void func test_override_func_2() -> void --- C --- @@ -53,8 +53,8 @@ func test_override_func_1() -> void func test_override_func_2() -> void func test_func_b1() -> void func test_func_b2() -> void -abstract func test_abstract_func_1() -> void -abstract func test_abstract_func_2() -> void +@abstract func test_abstract_func_1() -> void +@abstract func test_abstract_func_2() -> void func test_override_func_1() -> void func test_override_func_2() -> void === Signals === diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd index 582fb0c5fa9..7567a2ac0b6 100644 --- a/modules/gdscript/tests/scripts/utils.notest.gd +++ b/modules/gdscript/tests/scripts/utils.notest.gd @@ -127,7 +127,7 @@ static func print_property_extended_info( static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String: var result: String = "" if method.flags & METHOD_FLAG_VIRTUAL_REQUIRED: - result += "abstract " + result += "@abstract " if method.flags & METHOD_FLAG_STATIC: result += "static " result += ("signal " if is_signal else "func ") + method.name + "("