Merge pull request #107717 from aaronfranke/abstract-annotation

GDScript: Replace `abstract` keyword with `@abstract` annotation
This commit is contained in:
Rémi Verschelde 2025-06-27 17:12:56 +02:00 committed by GitHub
commit ebc36a7225
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 179 additions and 197 deletions

View file

@ -296,6 +296,27 @@
</constant> </constant>
</constants> </constants>
<annotations> <annotations>
<annotation name="@abstract">
<return type="void" />
<description>
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]
</description>
</annotation>
<annotation name="@export"> <annotation name="@export">
<return type="void" /> <return type="void" />
<description> <description>

View file

@ -2749,7 +2749,6 @@ Vector<String> GDScriptLanguage::get_reserved_words() const {
"when", "when",
"while", "while",
// Declarations. // Declarations.
"abstract",
"class", "class",
"class_name", "class_name",
"const", "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; *r_icon_path = c->simplified_icon_path;
} }
if (r_is_abstract) { if (r_is_abstract) {
*r_is_abstract = false; *r_is_abstract = c->is_abstract;
} }
if (r_is_tool) { if (r_is_tool) {
*r_is_tool = parser.is_tool(); *r_is_tool = parser.is_tool();

View file

@ -1540,12 +1540,12 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
if (member.function->is_abstract) { if (member.function->is_abstract) {
if (base_class == p_class) { if (base_class == p_class) {
const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name); 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; break;
} else if (!implemented_funcs.has(member.function->identifier->name)) { } 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 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); 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; break;
} }
} else { } else {
@ -1987,6 +1987,20 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun
} }
p_function->resolved_body = true; 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; GDScriptParser::FunctionNode *previous_function = parser->current_function;
parser->current_function = p_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. // Use the suite inferred type if return isn't explicitly set.
p_function->set_datatype(p_function->body->get_datatype()); 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)) { } 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); push_error(R"(Not all code paths return a value.)", p_function);
} }
} }

View file

@ -1532,7 +1532,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context
static const char *_keywords_with_space[] = { static const char *_keywords_with_space[] = {
"and", "not", "or", "in", "as", "class", "class_name", "extends", "is", "func", "signal", "await", "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 nullptr
}; };

View file

@ -96,6 +96,7 @@ GDScriptParser::GDScriptParser() {
register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation); 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("@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("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
register_annotation(MethodInfo("@abstract"), AnnotationInfo::SCRIPT | AnnotationInfo::CLASS | AnnotationInfo::FUNCTION, &GDScriptParser::abstract_annotation);
// Onready annotation. // Onready annotation.
register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
// Export annotations. // Export annotations.
@ -628,7 +629,7 @@ void GDScriptParser::parse_program() {
annotation_stack.push_back(annotation); annotation_stack.push_back(annotation);
} else if (annotation->applies_to(AnnotationInfo::SCRIPT)) { } else if (annotation->applies_to(AnnotationInfo::SCRIPT)) {
PUSH_PENDING_ANNOTATIONS_TO_HEAD; 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. // Some annotations need to be resolved and applied in the parser.
// The root class is not in any class, so `head->outer == nullptr`. // The root class is not in any class, so `head->outer == nullptr`.
annotation->apply(this, head, nullptr); annotation->apply(this, head, nullptr);
@ -675,52 +676,9 @@ void GDScriptParser::parse_program() {
reset_extents(head, current); reset_extents(head, current);
} }
bool first_is_abstract = false;
while (can_have_class_or_extends) { while (can_have_class_or_extends) {
// Order here doesn't matter, but there should be only one of each at most. // Order here doesn't matter, but there should be only one of each at most.
switch (current.type) { 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: case GDScriptTokenizer::Token::CLASS_NAME:
PUSH_PENDING_ANNOTATIONS_TO_HEAD; PUSH_PENDING_ANNOTATIONS_TO_HEAD;
advance(); 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. #undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
// It really speed up the call to GDScriptLanguage::get_global_class_name especially for large script.
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) { if (!parse_body) {
return; return;
} }
#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD parse_class_body(true);
parse_class_body(first_is_abstract, true);
head->end_line = current.end_line; head->end_line = current.end_line;
head->end_column = current.end_column; head->end_column = current.end_column;
@ -876,13 +842,12 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const {
return false; 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>(); ClassNode *n_class = alloc_node<ClassNode>();
ClassNode *previous_class = current_class; ClassNode *previous_class = current_class;
current_class = n_class; current_class = n_class;
n_class->outer = previous_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".)")) { if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the class name after "class".)")) {
n_class->identifier = parse_identifier(); n_class->identifier = parse_identifier();
@ -919,7 +884,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, bool
end_statement("superclass"); end_statement("superclass");
} }
parse_class_body(false, multiline); parse_class_body(multiline);
complete_extents(n_class); complete_extents(n_class);
if (multiline) { if (multiline) {
@ -978,7 +943,7 @@ void GDScriptParser::parse_extends() {
} }
template <typename T> template <typename T>
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(); advance();
// Consume annotations. // 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) { if (member == nullptr) {
return; 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; 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; bool next_is_static = false;
while (!class_end && !is_at_end()) { while (!class_end && !is_at_end()) {
GDScriptTokenizer::Token token = current; GDScriptTokenizer::Token token = current;
switch (token.type) { 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: 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) { if (next_is_static) {
current_class->has_static_data = true; 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"); parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
break; break;
case GDScriptTokenizer::Token::FUNC: 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; break;
case GDScriptTokenizer::Token::CLASS: 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; break;
case GDScriptTokenizer::Token::ENUM: case GDScriptTokenizer::Token::ENUM:
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "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; break;
} }
if (token.type != GDScriptTokenizer::Token::ABSTRACT) {
next_is_abstract = false;
}
if (token.type != GDScriptTokenizer::Token::STATIC) { if (token.type != GDScriptTokenizer::Token::STATIC) {
next_is_static = false; 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) { GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) {
return parse_variable(p_is_abstract, p_is_static, true); 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<VariableNode>(); VariableNode *variable = alloc_node<VariableNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) { 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<ConstantNode>(); ConstantNode *constant = alloc_node<ConstantNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) { if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
@ -1482,7 +1435,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() {
return 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<SignalNode>(); SignalNode *signal = alloc_node<SignalNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) { 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; 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<EnumNode>(); EnumNode *enum_node = alloc_node<EnumNode>();
bool named = false; bool named = false;
@ -1625,7 +1578,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_
return enum_node; 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()) { if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) {
bool default_used = false; bool default_used = false;
do { do {
@ -1704,17 +1657,16 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
} }
#endif // TOOLS_ENABLED #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. // 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)); 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<FunctionNode>(); FunctionNode *function = alloc_node<FunctionNode>();
function->is_abstract = p_is_abstract;
function->is_static = p_is_static; function->is_static = p_is_static;
make_completion_context(COMPLETION_OVERRIDE_METHOD, function); 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.)"); consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)");
#ifdef TOOLS_ENABLED #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 #else // !TOOLS_ENABLED
parse_function_signature(function, body, "function", -1); const bool has_body = parse_function_signature(function, body, "function", -1);
#endif // TOOLS_ENABLED #endif // TOOLS_ENABLED
current_suite = previous_suite; 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; function->min_local_doc_line = previous.end_line + 1;
#endif // TOOLS_ENABLED #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); reset_extents(body, current);
complete_extents(body); complete_extents(body);
function->body = body; function->body = body;
@ -1995,11 +1949,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
break; break;
case GDScriptTokenizer::Token::VAR: case GDScriptTokenizer::Token::VAR:
advance(); advance();
result = parse_variable(false, false, false); result = parse_variable(false, false);
break; break;
case GDScriptTokenizer::Token::TK_CONST: case GDScriptTokenizer::Token::TK_CONST:
advance(); advance();
result = parse_constant(false, false); result = parse_constant(false);
break; break;
case GDScriptTokenizer::Token::IF: case GDScriptTokenizer::Token::IF:
advance(); advance();
@ -4220,7 +4174,6 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty
{ nullptr, nullptr, PREC_NONE }, // MATCH, { nullptr, nullptr, PREC_NONE }, // MATCH,
{ nullptr, nullptr, PREC_NONE }, // WHEN, { nullptr, nullptr, PREC_NONE }, // WHEN,
// Keywords // Keywords
{ nullptr, nullptr, PREC_NONE }, // ABSTRACT
{ nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS, { nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS,
{ nullptr, nullptr, PREC_NONE }, // ASSERT, { nullptr, nullptr, PREC_NONE }, // ASSERT,
{ &GDScriptParser::parse_await, nullptr, PREC_NONE }, // AWAIT, { &GDScriptParser::parse_await, nullptr, PREC_NONE }, // AWAIT,
@ -4410,6 +4363,33 @@ bool GDScriptParser::static_unload_annotation(AnnotationNode *p_annotation, Node
return true; 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<ClassNode *>(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<FunctionNode *>(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) { 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.)"); 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) { void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
if (p_class->is_abstract) { for (const AnnotationNode *E : p_class->annotations) {
push_text("Abstract "); print_annotation(E);
} }
push_text("Class "); push_text("Class ");
if (p_class->identifier == nullptr) { 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) { for (const AnnotationNode *E : p_function->annotations) {
print_annotation(E); print_annotation(E);
} }
if (p_function->is_abstract) {
push_text("Abstract ");
}
if (p_function->is_static) { if (p_function->is_static) {
push_text("Static "); push_text("Static ");
} }

View file

@ -1503,17 +1503,17 @@ private:
// Main blocks. // Main blocks.
void parse_program(); 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_class_name();
void parse_extends(); 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 <typename T> template <typename T>
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); 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_abstract, bool p_is_static); SignalNode *parse_signal(bool p_is_static);
EnumNode *parse_enum(bool p_is_abstract, bool p_is_static); EnumNode *parse_enum(bool p_is_static);
ParameterNode *parse_parameter(); ParameterNode *parse_parameter();
FunctionNode *parse_function(bool p_is_abstract, bool p_is_static); FunctionNode *parse_function(bool p_is_static);
void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start); 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); SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
// Annotations // Annotations
AnnotationNode *parse_annotation(uint32_t p_valid_targets); 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 tool_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool icon_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 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); bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
template <PropertyHint t_hint, Variant::Type t_type> template <PropertyHint t_hint, Variant::Type t_type>
bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); 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); bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
// Statements. // Statements.
Node *parse_statement(); Node *parse_statement();
VariableNode *parse_variable(bool p_is_abstract, bool p_is_static); VariableNode *parse_variable(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, bool p_allow_property);
VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent); VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
void parse_property_getter(VariableNode *p_variable); void parse_property_getter(VariableNode *p_variable);
void parse_property_setter(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(); AssertNode *parse_assert();
BreakNode *parse_break(); BreakNode *parse_break();
ContinueNode *parse_continue(); ContinueNode *parse_continue();

View file

@ -101,7 +101,6 @@ static const char *token_names[] = {
"match", // MATCH, "match", // MATCH,
"when", // WHEN, "when", // WHEN,
// Keywords // Keywords
"abstract", // ABSTRACT,
"as", // AS, "as", // AS,
"assert", // ASSERT, "assert", // ASSERT,
"await", // AWAIT, "await", // AWAIT,
@ -200,7 +199,6 @@ bool GDScriptTokenizer::Token::is_identifier() const {
case IDENTIFIER: case IDENTIFIER:
case MATCH: // Used in String.match(). case MATCH: // Used in String.match().
case WHEN: // New keyword, avoid breaking existing code. case WHEN: // New keyword, avoid breaking existing code.
case ABSTRACT:
// Allow constants to be treated as regular identifiers. // Allow constants to be treated as regular identifiers.
case CONST_PI: case CONST_PI:
case CONST_INF: 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. // This is meant to allow keywords with the $ notation, but not as general identifiers.
switch (type) { switch (type) {
case IDENTIFIER: case IDENTIFIER:
case ABSTRACT:
case AND: case AND:
case AS: case AS:
case ASSERT: case ASSERT:
@ -491,7 +488,6 @@ GDScriptTokenizer::Token GDScriptTokenizerText::annotation() {
#define KEYWORDS(KEYWORD_GROUP, KEYWORD) \ #define KEYWORDS(KEYWORD_GROUP, KEYWORD) \
KEYWORD_GROUP('a') \ KEYWORD_GROUP('a') \
KEYWORD("abstract", Token::ABSTRACT) \
KEYWORD("as", Token::AS) \ KEYWORD("as", Token::AS) \
KEYWORD("and", Token::AND) \ KEYWORD("and", Token::AND) \
KEYWORD("assert", Token::ASSERT) \ KEYWORD("assert", Token::ASSERT) \

View file

@ -106,7 +106,6 @@ public:
MATCH, MATCH,
WHEN, WHEN,
// Keywords // Keywords
ABSTRACT,
AS, AS,
ASSERT, ASSERT,
AWAIT, AWAIT,

View file

@ -1,15 +1,15 @@
abstract class AbstractClass: @abstract class AbstractClass:
abstract func some_func() @abstract func some_func()
class ImplementedClass extends AbstractClass: class ImplementedClass extends AbstractClass:
func some_func(): func some_func():
pass pass
abstract class AbstractClassAgain extends ImplementedClass: @abstract class AbstractClassAgain extends ImplementedClass:
abstract func some_func() @abstract func some_func()
class Test1: class Test1:
abstract func some_func() @abstract func some_func()
class Test2 extends AbstractClass: class Test2 extends AbstractClass:
pass pass
@ -24,5 +24,18 @@ class Test4 extends AbstractClass:
func other_func(): func other_func():
super.some_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(): func test():
pass pass

View file

@ -1,6 +1,11 @@
GDTEST_ANALYZER_ERROR 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 37: "@abstract" annotation can only be used once per 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 28: "@abstract" annotation can only be used once per function.
>> ERROR at line 17: Class "Test3" must implement "AbstractClassAgain.some_func()" and other inherited abstract methods or be marked as abstract. >> 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 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 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".

View file

@ -2,7 +2,7 @@ extends RefCounted
const AbstractScript = preload("./construct_abstract_script.notest.gd") const AbstractScript = preload("./construct_abstract_script.notest.gd")
abstract class AbstractClass: @abstract class AbstractClass:
pass pass
func test(): func test():

View file

@ -1 +1 @@
abstract class_name AbstractScript @abstract class_name AbstractScript

View file

@ -8,7 +8,7 @@ class B extends A:
class C extends CanvasItem: class C extends CanvasItem:
pass pass
abstract class X: @abstract class X:
pass pass
class Y extends X: class Y extends X:

View file

@ -1,5 +1,5 @@
abstract class A: @abstract class A:
abstract func test(x: int) -> void @abstract func test(x: int) -> void
class B extends A: class B extends A:
func func

View file

@ -1,8 +0,0 @@
extends RefCounted
abstract class A:
abstract func f():
pass
func test():
pass

View file

@ -1,2 +0,0 @@
GDTEST_PARSER_ERROR
Expected end of statement after abstract function declaration, found ":" instead.

View file

@ -1,8 +0,0 @@
extends RefCounted
abstract class A:
# Currently, an abstract function cannot be static.
abstract static func f()
func test():
pass

View file

@ -1,2 +0,0 @@
GDTEST_PARSER_ERROR
Expected "class" or "func" after "abstract".

View file

@ -1,2 +1,2 @@
GDTEST_PARSER_ERROR GDTEST_PARSER_ERROR
Expected ":" after function declaration. Expected end of statement after bodyless function declaration, found "{" instead.

View file

@ -1,7 +0,0 @@
extends RefCounted
abstract abstract class A:
pass
func test():
pass

View file

@ -1,2 +0,0 @@
GDTEST_PARSER_ERROR
Expected "class_name", "extends", "class", or "func" after "abstract".

View file

@ -1,7 +0,0 @@
extends RefCounted
abstract class A:
abstract abstract func f()
func test():
pass

View file

@ -1,2 +0,0 @@
GDTEST_PARSER_ERROR
Expected "class" or "func" after "abstract".

View file

@ -0,0 +1,3 @@
# No colon --v
var f = func () -> void
pass

View file

@ -0,0 +1,2 @@
GDTEST_PARSER_ERROR
Expected ":" after lambda declaration.

View file

@ -1,8 +0,0 @@
extends RefCounted
abstract class A:
# Currently, an abstract function cannot be static.
static abstract func f()
func test():
pass

View file

@ -1,2 +0,0 @@
GDTEST_PARSER_ERROR
Expected "func" or "var" after "static".

View file

@ -1,18 +1,18 @@
abstract class A: @abstract class A:
abstract func get_text_1() -> String @abstract func get_text_1() -> String
abstract func get_text_2() -> String @abstract func get_text_2() -> String
# No `UNUSED_PARAMETER` warning. # No `UNUSED_PARAMETER` warning.
abstract func func_with_param(param: int) -> int @abstract func func_with_param(param: int) -> int
abstract func func_with_rest_param(...args: Array) -> int @abstract func func_with_rest_param(...args: Array) -> int
abstract func func_with_semicolon() -> int; @abstract func func_with_semicolon() -> int;
abstract func func_1() -> int; abstract func func_2() -> int @abstract func func_1() -> int; @abstract func func_2() -> int
abstract func func_without_return_type() @abstract func func_without_return_type()
func print_text_1() -> void: func print_text_1() -> void:
print(get_text_1()) print(get_text_1())
abstract class B extends A: @abstract class B extends A:
func get_text_1() -> String: func get_text_1() -> String:
return "text_1b" return "text_1b"
@ -30,8 +30,8 @@ class C extends B:
func func_2() -> int: return 0 func func_2() -> int: return 0
func func_without_return_type(): pass func func_without_return_type(): pass
abstract class D extends C: @abstract class D extends C:
abstract func get_text_1() -> String @abstract func get_text_1() -> String
func get_text_2() -> String: func get_text_2() -> String:
return super() + " text_2d" return super() + " text_2d"

View file

@ -2,9 +2,9 @@
@warning_ignore_start("unused_signal") @warning_ignore_start("unused_signal")
abstract class A: @abstract class A:
abstract func test_abstract_func_1() @abstract func test_abstract_func_1()
abstract func test_abstract_func_2() @abstract func test_abstract_func_2()
func test_override_func_1(): pass func test_override_func_1(): pass
func test_override_func_2(): pass func test_override_func_2(): pass

View file

@ -32,8 +32,8 @@ func test_override_func_1() -> void
func test_override_func_2() -> void func test_override_func_2() -> void
func test_func_b1() -> void func test_func_b1() -> void
func test_func_b2() -> void func test_func_b2() -> void
abstract func test_abstract_func_1() -> void @abstract func test_abstract_func_1() -> void
abstract func test_abstract_func_2() -> void @abstract func test_abstract_func_2() -> void
func test_override_func_1() -> void func test_override_func_1() -> void
func test_override_func_2() -> void func test_override_func_2() -> void
--- C --- --- C ---
@ -53,8 +53,8 @@ func test_override_func_1() -> void
func test_override_func_2() -> void func test_override_func_2() -> void
func test_func_b1() -> void func test_func_b1() -> void
func test_func_b2() -> void func test_func_b2() -> void
abstract func test_abstract_func_1() -> void @abstract func test_abstract_func_1() -> void
abstract func test_abstract_func_2() -> void @abstract func test_abstract_func_2() -> void
func test_override_func_1() -> void func test_override_func_1() -> void
func test_override_func_2() -> void func test_override_func_2() -> void
=== Signals === === Signals ===

View file

@ -127,7 +127,7 @@ static func print_property_extended_info(
static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String: static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String:
var result: String = "" var result: String = ""
if method.flags & METHOD_FLAG_VIRTUAL_REQUIRED: if method.flags & METHOD_FLAG_VIRTUAL_REQUIRED:
result += "abstract " result += "@abstract "
if method.flags & METHOD_FLAG_STATIC: if method.flags & METHOD_FLAG_STATIC:
result += "static " result += "static "
result += ("signal " if is_signal else "func ") + method.name + "(" result += ("signal " if is_signal else "func ") + method.name + "("