Skip to content

Commit 67fe051

Browse files
aaronfrankedalexeev
andcommitted
Add a keyword for abstract classes in GDScript
Co-authored-by: Danil Alexeev <danil@alexeev.xyz>
1 parent 0028fd6 commit 67fe051

17 files changed

+136
-42
lines changed

doc/classes/ScriptExtension.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
<method name="_is_abstract" qualifiers="virtual const">
163163
<return type="bool" />
164164
<description>
165-
Returns [code]true[/code] if the script is an abstract script. An abstract script does not have a constructor and cannot be instantiated.
165+
Returns [code]true[/code] if the script is an abstract script. Abstract scripts cannot be instantiated directly, instead other scripts should inherit them. Abstract scripts will be either unselectable or hidden in the Create New Node dialog (unselectable if there are non-abstract classes inheriting it, otherwise hidden).
166166
</description>
167167
</method>
168168
<method name="_is_placeholder_fallback_enabled" qualifiers="virtual const">

editor/create_dialog.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ bool CreateDialog::_should_hide_type(const StringName &p_type) const {
195195
i = script_path.find_char('/', i + 1);
196196
}
197197
}
198+
// Abstract scripts cannot be instantiated.
199+
String path = ScriptServer::get_global_class_path(p_type);
200+
Ref<Script> scr = ResourceLoader::load(path, "Script");
201+
return scr->is_abstract();
198202
}
199203

200204
return false;

modules/gdscript/gdscript.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -2743,6 +2743,7 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const {
27432743
"when",
27442744
"while",
27452745
// Declarations.
2746+
"abstract",
27462747
"class",
27472748
"class_name",
27482749
"const",

modules/gdscript/gdscript.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class GDScript : public Script {
6262
bool tool = false;
6363
bool valid = false;
6464
bool reloading = false;
65+
bool _is_abstract = false;
6566

6667
struct MemberInfo {
6768
int index = 0;
@@ -244,7 +245,6 @@ class GDScript : public Script {
244245
void clear(GDScript::ClearData *p_clear_data = nullptr);
245246

246247
virtual bool is_valid() const override { return valid; }
247-
virtual bool is_abstract() const override { return false; } // GDScript does not support abstract classes.
248248

249249
bool inherits_script(const Ref<Script> &p_script) const override;
250250

@@ -277,6 +277,7 @@ class GDScript : public Script {
277277
virtual void get_script_signal_list(List<MethodInfo> *r_signals) const override;
278278

279279
bool is_tool() const override { return tool; }
280+
bool is_abstract() const override { return _is_abstract; }
280281
Ref<GDScript> get_base() const;
281282

282283
const HashMap<StringName, MemberInfo> &debug_get_member_indices() const { return member_indices; }

modules/gdscript/gdscript_analyzer.cpp

+9-4
Original file line numberDiff line numberDiff line change
@@ -3562,10 +3562,15 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
35623562

35633563
bool is_constructor = (base_type.is_meta_type || (p_call->callee && p_call->callee->type == GDScriptParser::Node::IDENTIFIER)) && p_call->function_name == SNAME("new");
35643564

3565-
if (is_constructor && Engine::get_singleton()->has_singleton(base_type.native_type)) {
3566-
push_error(vformat(R"(Cannot construct native class "%s" because it is an engine singleton.)", base_type.native_type), p_call);
3567-
p_call->set_datatype(call_type);
3568-
return;
3565+
if (is_constructor) {
3566+
if (Engine::get_singleton()->has_singleton(base_type.native_type)) {
3567+
push_error(vformat(R"(Cannot construct native class "%s" because it is an engine singleton.)", base_type.native_type), p_call);
3568+
p_call->set_datatype(call_type);
3569+
return;
3570+
}
3571+
if ((base_type.kind == GDScriptParser::DataType::CLASS && base_type.class_type->is_abstract) || (base_type.kind == GDScriptParser::DataType::SCRIPT && base_type.script_type.is_valid() && base_type.script_type->is_abstract())) {
3572+
push_error(vformat(R"(Cannot construct abstract class "%s".)", base_type.to_string()), p_call);
3573+
}
35693574
}
35703575

35713576
if (get_function_signature(p_call, is_constructor, base_type, p_call->function_name, return_type, par_types, default_arg_count, method_flags)) {

modules/gdscript/gdscript_compiler.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,7 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP
27082708
p_script->clearing = false;
27092709

27102710
p_script->tool = parser->is_tool();
2711+
p_script->_is_abstract = p_class->is_abstract;
27112712

27122713
if (p_script->local_name != StringName()) {
27132714
if (ClassDB::class_exists(p_script->local_name) && ClassDB::is_class_exposed(p_script->local_name)) {

modules/gdscript/gdscript_editor.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -1479,7 +1479,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context
14791479

14801480
static const char *_keywords_with_space[] = {
14811481
"and", "not", "or", "in", "as", "class", "class_name", "extends", "is", "func", "signal", "await",
1482-
"const", "enum", "static", "var", "if", "elif", "else", "for", "match", "when", "while",
1482+
"const", "enum", "abstract", "static", "var", "if", "elif", "else", "for", "match", "when", "while",
14831483
nullptr
14841484
};
14851485

modules/gdscript/gdscript_parser.cpp

+72-25
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,8 @@ void GDScriptParser::parse_program() {
633633
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
634634
if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon")) {
635635
// Some annotations need to be resolved and applied in the parser.
636-
annotation->apply(this, head, nullptr); // `head->outer == nullptr`.
636+
// The root class is not in any class, so `head->outer == nullptr`.
637+
annotation->apply(this, head, nullptr);
637638
} else {
638639
head->annotations.push_back(annotation);
639640
}
@@ -677,9 +678,25 @@ void GDScriptParser::parse_program() {
677678
reset_extents(head, current);
678679
}
679680

681+
bool has_early_abstract = false;
680682
while (can_have_class_or_extends) {
681683
// Order here doesn't matter, but there should be only one of each at most.
682684
switch (current.type) {
685+
case GDScriptTokenizer::Token::ABSTRACT: {
686+
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
687+
if (head->start_line == 1) {
688+
reset_extents(head, current);
689+
}
690+
advance();
691+
if (has_early_abstract) {
692+
push_error(R"(Expected "class_name", "extends", or "class" after "abstract".)");
693+
} else {
694+
has_early_abstract = true;
695+
}
696+
if (current.type == GDScriptTokenizer::Token::NEWLINE) {
697+
end_statement("class_name abstract");
698+
}
699+
} break;
683700
case GDScriptTokenizer::Token::CLASS_NAME:
684701
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
685702
advance();
@@ -688,6 +705,10 @@ void GDScriptParser::parse_program() {
688705
} else {
689706
parse_class_name();
690707
}
708+
if (has_early_abstract) {
709+
head->is_abstract = true;
710+
has_early_abstract = false;
711+
}
691712
break;
692713
case GDScriptTokenizer::Token::EXTENDS:
693714
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
@@ -698,6 +719,10 @@ void GDScriptParser::parse_program() {
698719
parse_extends();
699720
end_statement("superclass");
700721
}
722+
if (has_early_abstract) {
723+
head->is_abstract = true;
724+
has_early_abstract = false;
725+
}
701726
break;
702727
case GDScriptTokenizer::Token::TK_EOF:
703728
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
@@ -732,7 +757,7 @@ void GDScriptParser::parse_program() {
732757

733758
#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
734759

735-
parse_class_body(true);
760+
parse_class_body(has_early_abstract, true);
736761

737762
head->end_line = current.end_line;
738763
head->end_column = current.end_column;
@@ -837,12 +862,13 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const {
837862
return false;
838863
}
839864

840-
GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
865+
GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, bool p_is_static) {
841866
ClassNode *n_class = alloc_node<ClassNode>();
842867

843868
ClassNode *previous_class = current_class;
844869
current_class = n_class;
845870
n_class->outer = previous_class;
871+
n_class->is_abstract = p_is_abstract;
846872

847873
if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the class name after "class".)")) {
848874
n_class->identifier = parse_identifier();
@@ -879,7 +905,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
879905
end_statement("superclass");
880906
}
881907

882-
parse_class_body(multiline);
908+
parse_class_body(false, multiline);
883909
complete_extents(n_class);
884910

885911
if (multiline) {
@@ -938,7 +964,7 @@ void GDScriptParser::parse_extends() {
938964
}
939965

940966
template <typename T>
941-
void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) {
967+
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) {
942968
advance();
943969

944970
// Consume annotations.
@@ -954,7 +980,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
954980
}
955981
}
956982

957-
T *member = (this->*p_parse_function)(p_is_static);
983+
T *member = (this->*p_parse_function)(p_is_abstract, p_is_static);
958984
if (member == nullptr) {
959985
return;
960986
}
@@ -1008,14 +1034,29 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
10081034
}
10091035
}
10101036

1011-
void GDScriptParser::parse_class_body(bool p_is_multiline) {
1037+
void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
10121038
bool class_end = false;
1039+
// The header parsing code might have skipped over abstract, so we start by checking the previous token.
1040+
bool next_is_abstract = p_is_abstract;
1041+
if (next_is_abstract && (current.type != GDScriptTokenizer::Token::CLASS_NAME && current.type != GDScriptTokenizer::Token::CLASS)) {
1042+
push_error(R"(Expected "class_name" or "class" after "abstract".)");
1043+
}
10131044
bool next_is_static = false;
10141045
while (!class_end && !is_at_end()) {
10151046
GDScriptTokenizer::Token token = current;
10161047
switch (token.type) {
1048+
case GDScriptTokenizer::Token::ABSTRACT: {
1049+
advance();
1050+
next_is_abstract = true;
1051+
if (check(GDScriptTokenizer::Token::NEWLINE)) {
1052+
advance();
1053+
}
1054+
if (!check(GDScriptTokenizer::Token::CLASS_NAME) && !check(GDScriptTokenizer::Token::CLASS)) {
1055+
push_error(R"(Expected "class_name" or "class" after "abstract".)");
1056+
}
1057+
} break;
10171058
case GDScriptTokenizer::Token::VAR:
1018-
parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static);
1059+
parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", false, next_is_static);
10191060
if (next_is_static) {
10201061
current_class->has_static_data = true;
10211062
}
@@ -1027,11 +1068,12 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
10271068
parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
10281069
break;
10291070
case GDScriptTokenizer::Token::FUNC:
1030-
parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_static);
1031-
break;
1032-
case GDScriptTokenizer::Token::CLASS:
1033-
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class");
1071+
parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", false, next_is_static);
10341072
break;
1073+
case GDScriptTokenizer::Token::CLASS: {
1074+
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class", next_is_abstract);
1075+
next_is_abstract = false;
1076+
} break;
10351077
case GDScriptTokenizer::Token::ENUM:
10361078
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
10371079
break;
@@ -1102,11 +1144,11 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
11021144
}
11031145
}
11041146

1105-
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) {
1106-
return parse_variable(p_is_static, true);
1147+
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_abstract, bool p_is_static) {
1148+
return parse_variable(p_is_abstract, p_is_static, true);
11071149
}
11081150

1109-
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) {
1151+
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_abstract, bool p_is_static, bool p_allow_property) {
11101152
VariableNode *variable = alloc_node<VariableNode>();
11111153

11121154
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
@@ -1342,7 +1384,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
13421384
}
13431385
}
13441386

1345-
GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) {
1387+
GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_abstract, bool p_is_static) {
13461388
ConstantNode *constant = alloc_node<ConstantNode>();
13471389

13481390
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
@@ -1410,7 +1452,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() {
14101452
return parameter;
14111453
}
14121454

1413-
GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
1455+
GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_abstract, bool p_is_static) {
14141456
SignalNode *signal = alloc_node<SignalNode>();
14151457

14161458
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) {
@@ -1455,7 +1497,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
14551497
return signal;
14561498
}
14571499

1458-
GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) {
1500+
GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_is_static) {
14591501
EnumNode *enum_node = alloc_node<EnumNode>();
14601502
bool named = false;
14611503

@@ -1608,7 +1650,7 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
16081650
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
16091651
}
16101652

1611-
GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
1653+
GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) {
16121654
FunctionNode *function = alloc_node<FunctionNode>();
16131655

16141656
make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
@@ -1873,11 +1915,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
18731915
break;
18741916
case GDScriptTokenizer::Token::VAR:
18751917
advance();
1876-
result = parse_variable(false, false);
1918+
result = parse_variable(false, false, false);
18771919
break;
18781920
case GDScriptTokenizer::Token::CONST:
18791921
advance();
1880-
result = parse_constant(false);
1922+
result = parse_constant(false, false);
18811923
break;
18821924
case GDScriptTokenizer::Token::IF:
18831925
advance();
@@ -4074,6 +4116,7 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty
40744116
{ nullptr, nullptr, PREC_NONE }, // MATCH,
40754117
{ nullptr, nullptr, PREC_NONE }, // WHEN,
40764118
// Keywords
4119+
{ nullptr, nullptr, PREC_NONE }, // ABSTRACT
40774120
{ nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS,
40784121
{ nullptr, nullptr, PREC_NONE }, // ASSERT,
40794122
{ &GDScriptParser::parse_await, nullptr, PREC_NONE }, // AWAIT,
@@ -5639,6 +5682,9 @@ void GDScriptParser::TreePrinter::print_cast(CastNode *p_cast) {
56395682
}
56405683

56415684
void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
5685+
if (p_class->is_abstract) {
5686+
push_text("Abstract ");
5687+
}
56425688
push_text("Class ");
56435689
if (p_class->identifier == nullptr) {
56445690
push_text("<unnamed>");
@@ -6264,17 +6310,18 @@ void GDScriptParser::TreePrinter::print_while(WhileNode *p_while) {
62646310
}
62656311

62666312
void GDScriptParser::TreePrinter::print_tree(const GDScriptParser &p_parser) {
6267-
ERR_FAIL_NULL_MSG(p_parser.get_tree(), "Parse the code before printing the parse tree.");
6313+
ClassNode *class_tree = p_parser.get_tree();
6314+
ERR_FAIL_NULL_MSG(class_tree, "Parse the code before printing the parse tree.");
62686315

62696316
if (p_parser.is_tool()) {
62706317
push_line("@tool");
62716318
}
6272-
if (!p_parser.get_tree()->icon_path.is_empty()) {
6319+
if (!class_tree->icon_path.is_empty()) {
62736320
push_text(R"(@icon (")");
6274-
push_text(p_parser.get_tree()->icon_path);
6321+
push_text(class_tree->icon_path);
62756322
push_line("\")");
62766323
}
6277-
print_class(p_parser.get_tree());
6324+
print_class(class_tree);
62786325

62796326
print_line(String(printed));
62806327
}

modules/gdscript/gdscript_parser.h

+10-9
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,7 @@ class GDScriptParser {
748748
ClassNode *outer = nullptr;
749749
bool extends_used = false;
750750
bool onready_used = false;
751+
bool is_abstract = false;
751752
bool has_static_data = false;
752753
bool annotated_static_unload = false;
753754
String extends_path;
@@ -1488,16 +1489,16 @@ class GDScriptParser {
14881489

14891490
// Main blocks.
14901491
void parse_program();
1491-
ClassNode *parse_class(bool p_is_static);
1492+
ClassNode *parse_class(bool p_is_abstract, bool p_is_static);
14921493
void parse_class_name();
14931494
void parse_extends();
1494-
void parse_class_body(bool p_is_multiline);
1495+
void parse_class_body(bool p_is_abstract, bool p_is_multiline);
14951496
template <typename T>
1496-
void parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static = false);
1497-
SignalNode *parse_signal(bool p_is_static);
1498-
EnumNode *parse_enum(bool p_is_static);
1497+
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);
1498+
SignalNode *parse_signal(bool p_is_abstract, bool p_is_static);
1499+
EnumNode *parse_enum(bool p_is_abstract, bool p_is_static);
14991500
ParameterNode *parse_parameter();
1500-
FunctionNode *parse_function(bool p_is_static);
1501+
FunctionNode *parse_function(bool p_is_abstract, bool p_is_static);
15011502
void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type);
15021503
SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
15031504
// Annotations
@@ -1521,12 +1522,12 @@ class GDScriptParser {
15211522
bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15221523
// Statements.
15231524
Node *parse_statement();
1524-
VariableNode *parse_variable(bool p_is_static);
1525-
VariableNode *parse_variable(bool p_is_static, bool p_allow_property);
1525+
VariableNode *parse_variable(bool p_is_abstract, bool p_is_static);
1526+
VariableNode *parse_variable(bool p_is_abstract, bool p_is_static, bool p_allow_property);
15261527
VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
15271528
void parse_property_getter(VariableNode *p_variable);
15281529
void parse_property_setter(VariableNode *p_variable);
1529-
ConstantNode *parse_constant(bool p_is_static);
1530+
ConstantNode *parse_constant(bool p_is_abstract, bool p_is_static);
15301531
AssertNode *parse_assert();
15311532
BreakNode *parse_break();
15321533
ContinueNode *parse_continue();

0 commit comments

Comments
 (0)