1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-13 13:31:48 +00:00

Merge pull request #67777 from aaronfranke/virtually-annotated

Add a keyword for abstract classes in GDScript
This commit is contained in:
Thaddeus Crews
2025-05-13 16:22:16 -05:00
17 changed files with 137 additions and 42 deletions

View File

@@ -162,7 +162,7 @@
<method name="_is_abstract" qualifiers="virtual const">
<return type="bool" />
<description>
Returns [code]true[/code] if the script is an abstract script. An abstract script does not have a constructor and cannot be instantiated.
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).
</description>
</method>
<method name="_is_placeholder_fallback_enabled" qualifiers="virtual const">

View File

@@ -219,6 +219,10 @@ bool CreateDialog::_should_hide_type(const StringName &p_type) const {
i = script_path.find_char('/', i + 1);
}
}
// Abstract scripts cannot be instantiated.
String path = ScriptServer::get_global_class_path(p_type);
Ref<Script> scr = ResourceLoader::load(path, "Script");
return scr.is_null() || scr->is_abstract();
}
return false;

View File

@@ -2747,6 +2747,7 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const {
"when",
"while",
// Declarations.
"abstract",
"class",
"class_name",
"const",

View File

@@ -62,6 +62,7 @@ class GDScript : public Script {
bool tool = false;
bool valid = false;
bool reloading = false;
bool _is_abstract = false;
struct MemberInfo {
int index = 0;
@@ -247,7 +248,6 @@ public:
void cancel_pending_functions(bool warn);
virtual bool is_valid() const override { return valid; }
virtual bool is_abstract() const override { return false; } // GDScript does not support abstract classes.
bool inherits_script(const Ref<Script> &p_script) const override;
@@ -280,6 +280,7 @@ public:
virtual void get_script_signal_list(List<MethodInfo> *r_signals) const override;
bool is_tool() const override { return tool; }
bool is_abstract() const override { return _is_abstract; }
Ref<GDScript> get_base() const;
const HashMap<StringName, MemberInfo> &debug_get_member_indices() const { return member_indices; }

View File

@@ -3573,11 +3573,16 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
bool is_constructor = (base_type.is_meta_type || (p_call->callee && p_call->callee->type == GDScriptParser::Node::IDENTIFIER)) && p_call->function_name == SNAME("new");
if (is_constructor && Engine::get_singleton()->has_singleton(base_type.native_type)) {
if (is_constructor) {
if (Engine::get_singleton()->has_singleton(base_type.native_type)) {
push_error(vformat(R"(Cannot construct native class "%s" because it is an engine singleton.)", base_type.native_type), p_call);
p_call->set_datatype(call_type);
return;
}
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())) {
push_error(vformat(R"(Cannot construct abstract class "%s".)", base_type.to_string()), p_call);
}
}
if (get_function_signature(p_call, is_constructor, base_type, p_call->function_name, return_type, par_types, default_arg_count, method_flags)) {
// If the method is implemented in the class hierarchy, the virtual flag will not be set for that MethodInfo and the search stops there.

View File

@@ -2710,6 +2710,7 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP
p_script->clearing = false;
p_script->tool = parser->is_tool();
p_script->_is_abstract = p_class->is_abstract;
if (p_script->local_name != StringName()) {
if (ClassDB::class_exists(p_script->local_name) && ClassDB::is_class_exposed(p_script->local_name)) {

View File

@@ -1491,7 +1491,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", "static", "var", "if", "elif", "else", "for", "match", "when", "while",
"const", "enum", "abstract", "static", "var", "if", "elif", "else", "for", "match", "when", "while",
nullptr
};

View File

@@ -633,7 +633,8 @@ void GDScriptParser::parse_program() {
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon")) {
// Some annotations need to be resolved and applied in the parser.
annotation->apply(this, head, nullptr); // `head->outer == nullptr`.
// The root class is not in any class, so `head->outer == nullptr`.
annotation->apply(this, head, nullptr);
} else {
head->annotations.push_back(annotation);
}
@@ -677,9 +678,25 @@ void GDScriptParser::parse_program() {
reset_extents(head, current);
}
bool has_early_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: {
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
if (head->start_line == 1) {
reset_extents(head, current);
}
advance();
if (has_early_abstract) {
push_error(R"(Expected "class_name", "extends", or "class" after "abstract".)");
} else {
has_early_abstract = true;
}
if (current.type == GDScriptTokenizer::Token::NEWLINE) {
end_statement("class_name abstract");
}
} break;
case GDScriptTokenizer::Token::CLASS_NAME:
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
advance();
@@ -688,6 +705,10 @@ void GDScriptParser::parse_program() {
} else {
parse_class_name();
}
if (has_early_abstract) {
head->is_abstract = true;
has_early_abstract = false;
}
break;
case GDScriptTokenizer::Token::EXTENDS:
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
@@ -698,6 +719,10 @@ void GDScriptParser::parse_program() {
parse_extends();
end_statement("superclass");
}
if (has_early_abstract) {
head->is_abstract = true;
has_early_abstract = false;
}
break;
case GDScriptTokenizer::Token::TK_EOF:
PUSH_PENDING_ANNOTATIONS_TO_HEAD;
@@ -732,7 +757,7 @@ void GDScriptParser::parse_program() {
#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
parse_class_body(true);
parse_class_body(has_early_abstract, true);
head->end_line = current.end_line;
head->end_column = current.end_column;
@@ -837,12 +862,13 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const {
return false;
}
GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, 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();
@@ -879,7 +905,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
end_statement("superclass");
}
parse_class_body(multiline);
parse_class_body(false, multiline);
complete_extents(n_class);
if (multiline) {
@@ -938,7 +964,7 @@ void GDScriptParser::parse_extends() {
}
template <typename T>
void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) {
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) {
advance();
// Consume annotations.
@@ -954,7 +980,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
}
}
T *member = (this->*p_parse_function)(p_is_static);
T *member = (this->*p_parse_function)(p_is_abstract, p_is_static);
if (member == nullptr) {
return;
}
@@ -1008,14 +1034,29 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
}
}
void GDScriptParser::parse_class_body(bool p_is_multiline) {
void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
bool class_end = false;
// The header parsing code might have skipped over abstract, so we start by checking the previous token.
bool next_is_abstract = p_is_abstract;
if (next_is_abstract && (current.type != GDScriptTokenizer::Token::CLASS_NAME && current.type != GDScriptTokenizer::Token::CLASS)) {
push_error(R"(Expected "class_name" or "class" after "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::NEWLINE)) {
advance();
}
if (!check(GDScriptTokenizer::Token::CLASS_NAME) && !check(GDScriptTokenizer::Token::CLASS)) {
push_error(R"(Expected "class_name" or "class" after "abstract".)");
}
} break;
case GDScriptTokenizer::Token::VAR:
parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static);
parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", false, next_is_static);
if (next_is_static) {
current_class->has_static_data = true;
}
@@ -1027,11 +1068,12 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
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_static);
break;
case GDScriptTokenizer::Token::CLASS:
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class");
parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", false, next_is_static);
break;
case GDScriptTokenizer::Token::CLASS: {
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class", next_is_abstract);
next_is_abstract = false;
} break;
case GDScriptTokenizer::Token::ENUM:
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
break;
@@ -1122,11 +1164,11 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
}
}
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) {
return parse_variable(p_is_abstract, p_is_static, true);
}
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) {
GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_abstract, bool p_is_static, bool p_allow_property) {
VariableNode *variable = alloc_node<VariableNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
@@ -1362,7 +1404,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
}
}
GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) {
GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_abstract, bool p_is_static) {
ConstantNode *constant = alloc_node<ConstantNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
@@ -1430,7 +1472,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() {
return parameter;
}
GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_abstract, bool p_is_static) {
SignalNode *signal = alloc_node<SignalNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) {
@@ -1475,7 +1517,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
return signal;
}
GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) {
GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_is_static) {
EnumNode *enum_node = alloc_node<EnumNode>();
bool named = false;
@@ -1628,7 +1670,7 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
}
GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) {
FunctionNode *function = alloc_node<FunctionNode>();
make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
@@ -1893,11 +1935,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
break;
case GDScriptTokenizer::Token::VAR:
advance();
result = parse_variable(false, false);
result = parse_variable(false, false, false);
break;
case GDScriptTokenizer::Token::TK_CONST:
advance();
result = parse_constant(false);
result = parse_constant(false, false);
break;
case GDScriptTokenizer::Token::IF:
advance();
@@ -4110,6 +4152,7 @@ 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,
@@ -5676,6 +5719,9 @@ void GDScriptParser::TreePrinter::print_cast(CastNode *p_cast) {
}
void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
if (p_class->is_abstract) {
push_text("Abstract ");
}
push_text("Class ");
if (p_class->identifier == nullptr) {
push_text("<unnamed>");
@@ -6301,17 +6347,18 @@ void GDScriptParser::TreePrinter::print_while(WhileNode *p_while) {
}
void GDScriptParser::TreePrinter::print_tree(const GDScriptParser &p_parser) {
ERR_FAIL_NULL_MSG(p_parser.get_tree(), "Parse the code before printing the parse tree.");
ClassNode *class_tree = p_parser.get_tree();
ERR_FAIL_NULL_MSG(class_tree, "Parse the code before printing the parse tree.");
if (p_parser.is_tool()) {
push_line("@tool");
}
if (!p_parser.get_tree()->icon_path.is_empty()) {
if (!class_tree->icon_path.is_empty()) {
push_text(R"(@icon (")");
push_text(p_parser.get_tree()->icon_path);
push_text(class_tree->icon_path);
push_line("\")");
}
print_class(p_parser.get_tree());
print_class(class_tree);
print_line(String(printed));
}

View File

@@ -748,6 +748,7 @@ public:
ClassNode *outer = nullptr;
bool extends_used = false;
bool onready_used = false;
bool is_abstract = false;
bool has_static_data = false;
bool annotated_static_unload = false;
String extends_path;
@@ -1499,16 +1500,16 @@ private:
// Main blocks.
void parse_program();
ClassNode *parse_class(bool p_is_static);
ClassNode *parse_class(bool p_is_abstract, bool p_is_static);
void parse_class_name();
void parse_extends();
void parse_class_body(bool p_is_multiline);
void parse_class_body(bool p_is_abstract, bool p_is_multiline);
template <typename T>
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);
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);
ParameterNode *parse_parameter();
FunctionNode *parse_function(bool p_is_static);
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);
SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
// Annotations
@@ -1532,12 +1533,12 @@ private:
bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
// Statements.
Node *parse_statement();
VariableNode *parse_variable(bool p_is_static);
VariableNode *parse_variable(bool p_is_static, bool p_allow_property);
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_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_static);
ConstantNode *parse_constant(bool p_is_abstract, bool p_is_static);
AssertNode *parse_assert();
BreakNode *parse_break();
ContinueNode *parse_continue();

View File

@@ -101,6 +101,7 @@ static const char *token_names[] = {
"match", // MATCH,
"when", // WHEN,
// Keywords
"abstract", // ABSTRACT,
"as", // AS,
"assert", // ASSERT,
"await", // AWAIT,
@@ -198,6 +199,7 @@ 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:
@@ -213,6 +215,7 @@ 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:
@@ -495,6 +498,7 @@ 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

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

View File

@@ -0,0 +1,10 @@
extends RefCounted
const AbstractScript = preload("./construct_abstract_script.notest.gd")
abstract class AbstractClass:
pass
func test():
var _a := AbstractScript.new()
var _b := AbstractClass.new()

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 9: Cannot construct abstract class "AbstractScript".
>> ERROR at line 10: Cannot construct abstract class "AbstractClass".

View File

@@ -0,0 +1 @@
abstract class_name AbstractScript

View File

@@ -8,5 +8,12 @@ class B extends A:
class C extends CanvasItem:
pass
abstract class X:
pass
class Y extends X:
func test() -> String:
return "ok"
func test():
print('ok')
print(Y.new().test())

View File

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

View File

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