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>
</constants>
<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">
<return type="void" />
<description>

View file

@ -2749,7 +2749,6 @@ Vector<String> 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();

View file

@ -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);
}
}

View file

@ -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
};

View file

@ -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>();
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 <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();
// 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<VariableNode>();
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>();
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<SignalNode>();
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<EnumNode>();
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<FunctionNode>();
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<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) {
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 ");
}

View file

@ -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 <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);
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 <PropertyHint t_hint, Variant::Type t_type>
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();

View file

@ -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) \

View file

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

View file

@ -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

View file

@ -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".

View file

@ -2,7 +2,7 @@ extends RefCounted
const AbstractScript = preload("./construct_abstract_script.notest.gd")
abstract class AbstractClass:
@abstract class AbstractClass:
pass
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:
pass
abstract class X:
@abstract class X:
pass
class Y extends X:

View file

@ -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

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
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 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"

View file

@ -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

View file

@ -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 ===

View file

@ -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 + "("