Skip to content

Commit ae9c078

Browse files
committed
Merge pull request godotengine#106409 from dalexeev/gds-add-abstract-methods
GDScript: Add abstract methods
2 parents 6427343 + a7cf206 commit ae9c078

27 files changed

+396
-91
lines changed

doc/classes/@GlobalScope.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3083,7 +3083,7 @@
30833083
Used internally. Allows to not dump core virtual methods (such as [method Object._notification]) to the JSON API.
30843084
</constant>
30853085
<constant name="METHOD_FLAG_VIRTUAL_REQUIRED" value="128" enum="MethodFlags" is_bitfield="true">
3086-
Flag for a virtual method that is required.
3086+
Flag for a virtual method that is required. In GDScript, this flag is set for abstract functions.
30873087
</constant>
30883088
<constant name="METHOD_FLAGS_DEFAULT" value="1" enum="MethodFlags" is_bitfield="true">
30893089
Default method flags (normal).

editor/editor_help.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ static void _add_qualifiers_to_rt(const String &p_qualifiers, RichTextLabel *p_r
154154
hint = TTR("This method has no side effects.\nIt does not modify the object in any way.");
155155
} else if (qualifier == "static") {
156156
hint = TTR("This method does not need an instance to be called.\nIt can be called directly using the class name.");
157+
} else if (qualifier == "abstract") {
158+
hint = TTR("This method must be implemented to complete the abstract class.");
157159
}
158160

159161
p_rt->add_text(" ");

modules/gdscript/editor/gdscript_docgen.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,15 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
407407
method_doc.deprecated_message = m_func->doc_data.deprecated_message;
408408
method_doc.is_experimental = m_func->doc_data.is_experimental;
409409
method_doc.experimental_message = m_func->doc_data.experimental_message;
410-
method_doc.qualifiers = m_func->is_static ? "static" : "";
410+
411+
// Currently, an abstract function cannot be static.
412+
if (m_func->is_abstract) {
413+
method_doc.qualifiers = "abstract";
414+
} else if (m_func->is_static) {
415+
method_doc.qualifiers = "static";
416+
} else {
417+
method_doc.qualifiers = "";
418+
}
411419

412420
if (func_name == "_init") {
413421
method_doc.return_type = "void";

modules/gdscript/gdscript_analyzer.cpp

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,44 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
15271527
resolve_pending_lambda_bodies();
15281528
}
15291529

1530+
// Resolve base abstract class/method implementation requirements.
1531+
if (!p_class->is_abstract) {
1532+
HashSet<StringName> implemented_funcs;
1533+
const GDScriptParser::ClassNode *base_class = p_class;
1534+
while (base_class != nullptr) {
1535+
if (!base_class->is_abstract && base_class != p_class) {
1536+
break;
1537+
}
1538+
for (GDScriptParser::ClassNode::Member member : base_class->members) {
1539+
if (member.type == GDScriptParser::ClassNode::Member::FUNCTION) {
1540+
if (member.function->is_abstract) {
1541+
if (base_class == p_class) {
1542+
const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name);
1543+
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);
1544+
break;
1545+
} else if (!implemented_funcs.has(member.function->identifier->name)) {
1546+
const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name);
1547+
const String base_class_name = base_class->identifier == nullptr ? base_class->fqcn.get_file() : String(base_class->identifier->name);
1548+
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);
1549+
break;
1550+
}
1551+
} else {
1552+
implemented_funcs.insert(member.function->identifier->name);
1553+
}
1554+
}
1555+
}
1556+
if (base_class->base_type.kind == GDScriptParser::DataType::CLASS) {
1557+
base_class = base_class->base_type.class_type;
1558+
} else if (base_class->base_type.kind == GDScriptParser::DataType::SCRIPT) {
1559+
Ref<GDScriptParserRef> base_parser_ref = parser->get_depended_parser_for(base_class->base_type.script_path);
1560+
ERR_BREAK(base_parser_ref.is_null());
1561+
base_class = base_parser_ref->get_parser()->head;
1562+
} else {
1563+
break;
1564+
}
1565+
}
1566+
}
1567+
15301568
parser->current_class = previous_class;
15311569
}
15321570

@@ -1741,7 +1779,7 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
17411779
resolve_parameter(p_function->parameters[i]);
17421780
method_info.arguments.push_back(p_function->parameters[i]->get_datatype().to_property_info(p_function->parameters[i]->identifier->name));
17431781
#ifdef DEBUG_ENABLED
1744-
if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_")) {
1782+
if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_") && !p_function->is_abstract) {
17451783
parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, function_visible_name, p_function->parameters[i]->identifier->name);
17461784
}
17471785
is_shadowing(p_function->parameters[i]->identifier, "function parameter", true);
@@ -1920,7 +1958,7 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun
19201958
// Use the suite inferred type if return isn't explicitly set.
19211959
p_function->set_datatype(p_function->body->get_datatype());
19221960
} 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)) {
1923-
if (!p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) {
1961+
if (!p_function->is_abstract && !p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) {
19241962
push_error(R"(Not all code paths return a value.)", p_function);
19251963
}
19261964
}
@@ -3585,11 +3623,15 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
35853623
}
35863624

35873625
if (get_function_signature(p_call, is_constructor, base_type, p_call->function_name, return_type, par_types, default_arg_count, method_flags)) {
3588-
// If the method is implemented in the class hierarchy, the virtual flag will not be set for that MethodInfo and the search stops there.
3589-
// Virtual check only possible for super() calls because class hierarchy is known. Node/Objects may have scripts attached we don't know of at compile-time.
35903626
p_call->is_static = method_flags.has_flag(METHOD_FLAG_STATIC);
3591-
if (p_call->is_super && method_flags.has_flag(METHOD_FLAG_VIRTUAL)) {
3592-
push_error(vformat(R"*(Cannot call the parent class' virtual function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
3627+
// If the method is implemented in the class hierarchy, the virtual/abstract flag will not be set for that `MethodInfo` and the search stops there.
3628+
// Virtual/abstract check only possible for super calls because class hierarchy is known. Objects may have scripts attached we don't know of at compile-time.
3629+
if (p_call->is_super) {
3630+
if (method_flags.has_flag(METHOD_FLAG_VIRTUAL)) {
3631+
push_error(vformat(R"*(Cannot call the parent class' virtual function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
3632+
} else if (method_flags.has_flag(METHOD_FLAG_VIRTUAL_REQUIRED)) {
3633+
push_error(vformat(R"*(Cannot call the parent class' abstract function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
3634+
}
35933635
}
35943636

35953637
// If the function requires typed arrays we must make literals be typed.
@@ -5799,6 +5841,9 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bo
57995841
}
58005842

58015843
if (found_function != nullptr) {
5844+
if (found_function->is_abstract) {
5845+
r_method_flags.set_flag(METHOD_FLAG_VIRTUAL_REQUIRED);
5846+
}
58025847
if (p_is_constructor || found_function->is_static) {
58035848
r_method_flags.set_flag(METHOD_FLAG_STATIC);
58045849
}

modules/gdscript/gdscript_compiler.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
22542254
codegen.function_node = p_func;
22552255

22562256
StringName func_name;
2257+
bool is_abstract = false;
22572258
bool is_static = false;
22582259
Variant rpc_config;
22592260
GDScriptDataType return_type;
@@ -2267,6 +2268,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
22672268
} else {
22682269
func_name = "<anonymous lambda>";
22692270
}
2271+
is_abstract = p_func->is_abstract;
22702272
is_static = p_func->is_static;
22712273
rpc_config = p_func->rpc_config;
22722274
return_type = _gdtype_from_datatype(p_func->get_datatype(), p_script);
@@ -2283,6 +2285,9 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
22832285
codegen.function_name = func_name;
22842286
method_info.name = func_name;
22852287
codegen.is_static = is_static;
2288+
if (is_abstract) {
2289+
method_info.flags |= METHOD_FLAG_VIRTUAL_REQUIRED;
2290+
}
22862291
if (is_static) {
22872292
method_info.flags |= METHOD_FLAG_STATIC;
22882293
}

modules/gdscript/gdscript_parser.cpp

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -674,23 +674,50 @@ void GDScriptParser::parse_program() {
674674
reset_extents(head, current);
675675
}
676676

677-
bool has_early_abstract = false;
677+
bool first_is_abstract = false;
678678
while (can_have_class_or_extends) {
679679
// Order here doesn't matter, but there should be only one of each at most.
680680
switch (current.type) {
681681
case GDScriptTokenizer::Token::ABSTRACT: {
682-
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
683-
if (head->start_line == 1) {
684-
reset_extents(head, current);
682+
if (head->is_abstract) {
683+
// The root class is already marked as abstract, so this is
684+
// the beginning of an abstract function or inner class.
685+
can_have_class_or_extends = false;
686+
break;
685687
}
688+
689+
const GDScriptTokenizer::Token abstract_token = current;
686690
advance();
687-
if (has_early_abstract) {
688-
push_error(R"(Expected "class_name", "extends", or "class" after "abstract".)");
689-
} else {
690-
has_early_abstract = true;
691-
}
691+
692+
// A standalone "abstract" is only allowed for script-level stuff.
693+
bool is_standalone = false;
692694
if (current.type == GDScriptTokenizer::Token::NEWLINE) {
693-
end_statement("class_name abstract");
695+
is_standalone = true;
696+
end_statement("standalone \"abstract\"");
697+
}
698+
699+
switch (current.type) {
700+
case GDScriptTokenizer::Token::CLASS_NAME:
701+
case GDScriptTokenizer::Token::EXTENDS:
702+
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
703+
head->is_abstract = true;
704+
if (head->start_line == 1) {
705+
reset_extents(head, abstract_token);
706+
}
707+
break;
708+
case GDScriptTokenizer::Token::CLASS:
709+
case GDScriptTokenizer::Token::FUNC:
710+
if (is_standalone) {
711+
push_error(R"(Expected "class_name" or "extends" after a standalone "abstract".)");
712+
} else {
713+
first_is_abstract = true;
714+
}
715+
// This is the beginning of an abstract function or inner class.
716+
can_have_class_or_extends = false;
717+
break;
718+
default:
719+
push_error(R"(Expected "class_name", "extends", "class", or "func" after "abstract".)");
720+
break;
694721
}
695722
} break;
696723
case GDScriptTokenizer::Token::CLASS_NAME:
@@ -701,10 +728,6 @@ void GDScriptParser::parse_program() {
701728
} else {
702729
parse_class_name();
703730
}
704-
if (has_early_abstract) {
705-
head->is_abstract = true;
706-
has_early_abstract = false;
707-
}
708731
break;
709732
case GDScriptTokenizer::Token::EXTENDS:
710733
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
@@ -715,10 +738,6 @@ void GDScriptParser::parse_program() {
715738
parse_extends();
716739
end_statement("superclass");
717740
}
718-
if (has_early_abstract) {
719-
head->is_abstract = true;
720-
has_early_abstract = false;
721-
}
722741
break;
723742
case GDScriptTokenizer::Token::TK_EOF:
724743
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
@@ -753,7 +772,7 @@ void GDScriptParser::parse_program() {
753772

754773
#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
755774

756-
parse_class_body(has_early_abstract, true);
775+
parse_class_body(first_is_abstract, true);
757776

758777
head->end_line = current.end_line;
759778
head->end_column = current.end_column;
@@ -1028,25 +1047,19 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
10281047
}
10291048
}
10301049

1031-
void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
1050+
void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multiline) {
10321051
bool class_end = false;
1033-
// The header parsing code might have skipped over abstract, so we start by checking the previous token.
1034-
bool next_is_abstract = p_is_abstract;
1035-
if (next_is_abstract && (current.type != GDScriptTokenizer::Token::CLASS_NAME && current.type != GDScriptTokenizer::Token::CLASS)) {
1036-
push_error(R"(Expected "class_name" or "class" after "abstract".)");
1037-
}
1052+
// The header parsing code could consume `abstract` for the first function or inner class.
1053+
bool next_is_abstract = p_first_is_abstract;
10381054
bool next_is_static = false;
10391055
while (!class_end && !is_at_end()) {
10401056
GDScriptTokenizer::Token token = current;
10411057
switch (token.type) {
10421058
case GDScriptTokenizer::Token::ABSTRACT: {
10431059
advance();
10441060
next_is_abstract = true;
1045-
if (check(GDScriptTokenizer::Token::NEWLINE)) {
1046-
advance();
1047-
}
1048-
if (!check(GDScriptTokenizer::Token::CLASS_NAME) && !check(GDScriptTokenizer::Token::CLASS)) {
1049-
push_error(R"(Expected "class_name" or "class" after "abstract".)");
1061+
if (!check(GDScriptTokenizer::Token::CLASS) && !check(GDScriptTokenizer::Token::FUNC)) {
1062+
push_error(R"(Expected "class" or "func" after "abstract".)");
10501063
}
10511064
} break;
10521065
case GDScriptTokenizer::Token::VAR:
@@ -1062,12 +1075,11 @@ void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
10621075
parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
10631076
break;
10641077
case GDScriptTokenizer::Token::FUNC:
1065-
parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", false, next_is_static);
1078+
parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_abstract, next_is_static);
10661079
break;
1067-
case GDScriptTokenizer::Token::CLASS: {
1080+
case GDScriptTokenizer::Token::CLASS:
10681081
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class", next_is_abstract);
1069-
next_is_abstract = false;
1070-
} break;
1082+
break;
10711083
case GDScriptTokenizer::Token::ENUM:
10721084
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
10731085
break;
@@ -1146,6 +1158,9 @@ void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
11461158
}
11471159
break;
11481160
}
1161+
if (token.type != GDScriptTokenizer::Token::ABSTRACT) {
1162+
next_is_abstract = false;
1163+
}
11491164
if (token.type != GDScriptTokenizer::Token::STATIC) {
11501165
next_is_static = false;
11511166
}
@@ -1662,18 +1677,23 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
16621677

16631678
#ifdef TOOLS_ENABLED
16641679
if (p_type == "function" && p_signature_start != -1) {
1665-
int signature_end_pos = tokenizer->get_current_position() - 1;
1666-
String source_code = tokenizer->get_source_code();
1667-
p_function->signature = source_code.substr(p_signature_start, signature_end_pos - p_signature_start);
1680+
const int signature_end_pos = tokenizer->get_current_position() - 1;
1681+
const String source_code = tokenizer->get_source_code();
1682+
p_function->signature = source_code.substr(p_signature_start, signature_end_pos - p_signature_start).strip_edges(false, true);
16681683
}
16691684
#endif // TOOLS_ENABLED
16701685

1671-
// TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens.
1672-
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
1686+
if (p_function->is_abstract) {
1687+
end_statement("abstract function declaration");
1688+
} else {
1689+
// TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens.
1690+
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
1691+
}
16731692
}
16741693

16751694
GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) {
16761695
FunctionNode *function = alloc_node<FunctionNode>();
1696+
function->is_abstract = p_is_abstract;
16771697
function->is_static = p_is_static;
16781698

16791699
make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
@@ -1714,7 +1734,13 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract,
17141734
function->min_local_doc_line = previous.end_line + 1;
17151735
#endif // TOOLS_ENABLED
17161736

1717-
function->body = parse_suite("function declaration", body);
1737+
if (function->is_abstract) {
1738+
reset_extents(body, current);
1739+
complete_extents(body);
1740+
function->body = body;
1741+
} else {
1742+
function->body = parse_suite("function declaration", body);
1743+
}
17181744

17191745
current_function = previous_function;
17201746
complete_extents(function);
@@ -5936,6 +5962,9 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const
59365962
for (const AnnotationNode *E : p_function->annotations) {
59375963
print_annotation(E);
59385964
}
5965+
if (p_function->is_abstract) {
5966+
push_text("Abstract ");
5967+
}
59395968
if (p_function->is_static) {
59405969
push_text("Static ");
59415970
}

modules/gdscript/gdscript_parser.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,7 @@ class GDScriptParser {
853853
HashMap<StringName, int> parameters_indices;
854854
TypeNode *return_type = nullptr;
855855
SuiteNode *body = nullptr;
856+
bool is_abstract = false;
856857
bool is_static = false; // For lambdas it's determined in the analyzer.
857858
bool is_coroutine = false;
858859
Variant rpc_config;
@@ -1502,7 +1503,7 @@ class GDScriptParser {
15021503
ClassNode *parse_class(bool p_is_abstract, bool p_is_static);
15031504
void parse_class_name();
15041505
void parse_extends();
1505-
void parse_class_body(bool p_is_abstract, bool p_is_multiline);
1506+
void parse_class_body(bool p_first_is_abstract, bool p_is_multiline);
15061507
template <typename T>
15071508
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);
15081509
SignalNode *parse_signal(bool p_is_abstract, bool p_is_static);

0 commit comments

Comments
 (0)