diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index 918047485e..22b59e56a0 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -3083,7 +3083,7 @@
Used internally. Allows to not dump core virtual methods (such as [method Object._notification]) to the JSON API.
- Flag for a virtual method that is required.
+ Flag for a virtual method that is required. In GDScript, this flag is set for abstract functions.
Default method flags (normal).
diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp
index 10949e8246..02e2352611 100644
--- a/editor/editor_help.cpp
+++ b/editor/editor_help.cpp
@@ -154,6 +154,8 @@ static void _add_qualifiers_to_rt(const String &p_qualifiers, RichTextLabel *p_r
hint = TTR("This method has no side effects.\nIt does not modify the object in any way.");
} else if (qualifier == "static") {
hint = TTR("This method does not need an instance to be called.\nIt can be called directly using the class name.");
+ } else if (qualifier == "abstract") {
+ hint = TTR("This method must be implemented to complete the abstract class.");
}
p_rt->add_text(" ");
diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp
index 733de25885..053b1e2556 100644
--- a/modules/gdscript/editor/gdscript_docgen.cpp
+++ b/modules/gdscript/editor/gdscript_docgen.cpp
@@ -407,7 +407,15 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
method_doc.deprecated_message = m_func->doc_data.deprecated_message;
method_doc.is_experimental = m_func->doc_data.is_experimental;
method_doc.experimental_message = m_func->doc_data.experimental_message;
- method_doc.qualifiers = m_func->is_static ? "static" : "";
+
+ // Currently, an abstract function cannot be static.
+ if (m_func->is_abstract) {
+ method_doc.qualifiers = "abstract";
+ } else if (m_func->is_static) {
+ method_doc.qualifiers = "static";
+ } else {
+ method_doc.qualifiers = "";
+ }
if (func_name == "_init") {
method_doc.return_type = "void";
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index 7ddb8fe05b..93471172dc 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -1527,6 +1527,44 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
resolve_pending_lambda_bodies();
}
+ // Resolve base abstract class/method implementation requirements.
+ if (!p_class->is_abstract) {
+ HashSet implemented_funcs;
+ const GDScriptParser::ClassNode *base_class = p_class;
+ while (base_class != nullptr) {
+ if (!base_class->is_abstract && base_class != p_class) {
+ break;
+ }
+ for (GDScriptParser::ClassNode::Member member : base_class->members) {
+ if (member.type == GDScriptParser::ClassNode::Member::FUNCTION) {
+ 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);
+ 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);
+ break;
+ }
+ } else {
+ implemented_funcs.insert(member.function->identifier->name);
+ }
+ }
+ }
+ if (base_class->base_type.kind == GDScriptParser::DataType::CLASS) {
+ base_class = base_class->base_type.class_type;
+ } else if (base_class->base_type.kind == GDScriptParser::DataType::SCRIPT) {
+ Ref base_parser_ref = parser->get_depended_parser_for(base_class->base_type.script_path);
+ ERR_BREAK(base_parser_ref.is_null());
+ base_class = base_parser_ref->get_parser()->head;
+ } else {
+ break;
+ }
+ }
+ }
+
parser->current_class = previous_class;
}
@@ -1741,7 +1779,7 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
resolve_parameter(p_function->parameters[i]);
method_info.arguments.push_back(p_function->parameters[i]->get_datatype().to_property_info(p_function->parameters[i]->identifier->name));
#ifdef DEBUG_ENABLED
- if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_")) {
+ if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_") && !p_function->is_abstract) {
parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, function_visible_name, p_function->parameters[i]->identifier->name);
}
is_shadowing(p_function->parameters[i]->identifier, "function parameter", true);
@@ -1920,7 +1958,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->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) {
+ if (!p_function->is_abstract && !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);
}
}
@@ -3585,11 +3623,15 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
}
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.
- // 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.
p_call->is_static = method_flags.has_flag(METHOD_FLAG_STATIC);
- if (p_call->is_super && method_flags.has_flag(METHOD_FLAG_VIRTUAL)) {
- push_error(vformat(R"*(Cannot call the parent class' virtual function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
+ // 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.
+ // 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.
+ if (p_call->is_super) {
+ if (method_flags.has_flag(METHOD_FLAG_VIRTUAL)) {
+ push_error(vformat(R"*(Cannot call the parent class' virtual function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
+ } else if (method_flags.has_flag(METHOD_FLAG_VIRTUAL_REQUIRED)) {
+ push_error(vformat(R"*(Cannot call the parent class' abstract function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
+ }
}
// 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
}
if (found_function != nullptr) {
+ if (found_function->is_abstract) {
+ r_method_flags.set_flag(METHOD_FLAG_VIRTUAL_REQUIRED);
+ }
if (p_is_constructor || found_function->is_static) {
r_method_flags.set_flag(METHOD_FLAG_STATIC);
}
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index 739046bf68..74b04d36a8 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -2254,6 +2254,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
codegen.function_node = p_func;
StringName func_name;
+ bool is_abstract = false;
bool is_static = false;
Variant rpc_config;
GDScriptDataType return_type;
@@ -2267,6 +2268,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
} else {
func_name = "";
}
+ is_abstract = p_func->is_abstract;
is_static = p_func->is_static;
rpc_config = p_func->rpc_config;
return_type = _gdtype_from_datatype(p_func->get_datatype(), p_script);
@@ -2283,6 +2285,9 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
codegen.function_name = func_name;
method_info.name = func_name;
codegen.is_static = is_static;
+ if (is_abstract) {
+ method_info.flags |= METHOD_FLAG_VIRTUAL_REQUIRED;
+ }
if (is_static) {
method_info.flags |= METHOD_FLAG_STATIC;
}
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index 90568e8c32..1c17366fba 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -674,23 +674,50 @@ void GDScriptParser::parse_program() {
reset_extents(head, current);
}
- bool has_early_abstract = false;
+ 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: {
- PUSH_PENDING_ANNOTATIONS_TO_HEAD;
- if (head->start_line == 1) {
- reset_extents(head, current);
+ 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();
- if (has_early_abstract) {
- push_error(R"(Expected "class_name", "extends", or "class" after "abstract".)");
- } else {
- has_early_abstract = true;
- }
+
+ // A standalone "abstract" is only allowed for script-level stuff.
+ bool is_standalone = false;
if (current.type == GDScriptTokenizer::Token::NEWLINE) {
- end_statement("class_name abstract");
+ 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:
@@ -701,10 +728,6 @@ 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;
@@ -715,10 +738,6 @@ 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;
@@ -753,7 +772,7 @@ void GDScriptParser::parse_program() {
#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
- parse_class_body(has_early_abstract, true);
+ parse_class_body(first_is_abstract, true);
head->end_line = current.end_line;
head->end_column = current.end_column;
@@ -1028,13 +1047,10 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
}
}
-void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
+void GDScriptParser::parse_class_body(bool p_first_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".)");
- }
+ // 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;
@@ -1042,11 +1058,8 @@ void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
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".)");
+ if (!check(GDScriptTokenizer::Token::CLASS) && !check(GDScriptTokenizer::Token::FUNC)) {
+ push_error(R"(Expected "class" or "func" after "abstract".)");
}
} break;
case GDScriptTokenizer::Token::VAR:
@@ -1062,12 +1075,11 @@ void GDScriptParser::parse_class_body(bool p_is_abstract, 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", false, next_is_static);
+ parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_abstract, next_is_static);
break;
- case GDScriptTokenizer::Token::CLASS: {
+ case GDScriptTokenizer::Token::CLASS:
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class", next_is_abstract);
- next_is_abstract = false;
- } break;
+ break;
case GDScriptTokenizer::Token::ENUM:
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
break;
@@ -1146,6 +1158,9 @@ void GDScriptParser::parse_class_body(bool p_is_abstract, bool p_is_multiline) {
}
break;
}
+ if (token.type != GDScriptTokenizer::Token::ABSTRACT) {
+ next_is_abstract = false;
+ }
if (token.type != GDScriptTokenizer::Token::STATIC) {
next_is_static = false;
}
@@ -1662,18 +1677,23 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
#ifdef TOOLS_ENABLED
if (p_type == "function" && p_signature_start != -1) {
- int signature_end_pos = tokenizer->get_current_position() - 1;
- String source_code = tokenizer->get_source_code();
- p_function->signature = source_code.substr(p_signature_start, signature_end_pos - p_signature_start);
+ const int signature_end_pos = tokenizer->get_current_position() - 1;
+ const String source_code = tokenizer->get_source_code();
+ p_function->signature = source_code.substr(p_signature_start, signature_end_pos - p_signature_start).strip_edges(false, true);
}
#endif // TOOLS_ENABLED
- // 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_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));
+ }
}
GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) {
FunctionNode *function = alloc_node();
+ function->is_abstract = p_is_abstract;
function->is_static = p_is_static;
make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
@@ -1714,7 +1734,13 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract,
function->min_local_doc_line = previous.end_line + 1;
#endif // TOOLS_ENABLED
- function->body = parse_suite("function declaration", body);
+ if (function->is_abstract) {
+ reset_extents(body, current);
+ complete_extents(body);
+ function->body = body;
+ } else {
+ function->body = parse_suite("function declaration", body);
+ }
current_function = previous_function;
complete_extents(function);
@@ -5936,6 +5962,9 @@ 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 ");
}
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 3b400bfca1..848d20d9e7 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -853,6 +853,7 @@ public:
HashMap parameters_indices;
TypeNode *return_type = nullptr;
SuiteNode *body = nullptr;
+ bool is_abstract = false;
bool is_static = false; // For lambdas it's determined in the analyzer.
bool is_coroutine = false;
Variant rpc_config;
@@ -1502,7 +1503,7 @@ private:
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_abstract, bool p_is_multiline);
+ void parse_class_body(bool p_first_is_abstract, bool p_is_multiline);
template
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);
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd
new file mode 100644
index 0000000000..9e4e788f00
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd
@@ -0,0 +1,28 @@
+abstract class AbstractClass:
+ abstract func some_func()
+
+class ImplementedClass extends AbstractClass:
+ func some_func():
+ pass
+
+abstract class AbstractClassAgain extends ImplementedClass:
+ abstract func some_func()
+
+class Test1:
+ abstract func some_func()
+
+class Test2 extends AbstractClass:
+ pass
+
+class Test3 extends AbstractClassAgain:
+ pass
+
+class Test4 extends AbstractClass:
+ func some_func():
+ super()
+
+ func other_func():
+ super.some_func()
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out
new file mode 100644
index 0000000000..38dda90bf5
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out
@@ -0,0 +1,6 @@
+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 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.
diff --git a/modules/gdscript/tests/scripts/completion/common/override_function_abstract.cfg b/modules/gdscript/tests/scripts/completion/common/override_function_abstract.cfg
new file mode 100644
index 0000000000..7dbeadd7a7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/common/override_function_abstract.cfg
@@ -0,0 +1,4 @@
+[output]
+include=[
+ {"display": "test(x: int) -> void:", "insert_text": "test(x: int) -> void:"},
+]
diff --git a/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd b/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd
new file mode 100644
index 0000000000..09772812c5
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd
@@ -0,0 +1,5 @@
+abstract class A:
+ abstract func test(x: int) -> void
+
+class B extends A:
+ func ➡
diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd b/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd
new file mode 100644
index 0000000000..318f33f38e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd
@@ -0,0 +1,8 @@
+extends RefCounted
+
+abstract class A:
+ abstract func f():
+ pass
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out b/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out
new file mode 100644
index 0000000000..a3669b4912
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected end of statement after abstract function declaration, found ":" instead.
diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd b/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd
new file mode 100644
index 0000000000..227adde2e1
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd
@@ -0,0 +1,8 @@
+extends RefCounted
+
+abstract class A:
+ # Currently, an abstract function cannot be static.
+ abstract static func f()
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out b/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out
new file mode 100644
index 0000000000..1f481a5ed3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected "class" or "func" after "abstract".
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.out
deleted file mode 100644
index 5ebfac7c74..0000000000
--- a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.out
+++ /dev/null
@@ -1,2 +0,0 @@
-GDTEST_PARSER_ERROR
-Expected "class_name", "extends", or "class" after "abstract".
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.gd b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd
similarity index 100%
rename from modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.gd
rename to modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out
new file mode 100644
index 0000000000..220acdd43e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected "class_name", "extends", "class", or "func" after "abstract".
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd
new file mode 100644
index 0000000000..23eeb079fe
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd
@@ -0,0 +1,7 @@
+extends RefCounted
+
+abstract class A:
+ abstract abstract func f()
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out
new file mode 100644
index 0000000000..1f481a5ed3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected "class" or "func" after "abstract".
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd b/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd
new file mode 100644
index 0000000000..baa652b57b
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd
@@ -0,0 +1,8 @@
+extends RefCounted
+
+abstract class A:
+ # Currently, an abstract function cannot be static.
+ static abstract func f()
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out b/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out
new file mode 100644
index 0000000000..ff3edc4d6f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected "func" or "var" after "static".
diff --git a/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd b/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd
new file mode 100644
index 0000000000..5ade2fe990
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd
@@ -0,0 +1,48 @@
+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_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:
+ func get_text_1() -> String:
+ return "text_1b"
+
+ func print_text_2() -> void:
+ print(get_text_2())
+
+class C extends B:
+ func get_text_2() -> String:
+ return "text_2c"
+
+ func func_with_param(param: int) -> int: return param
+ func func_with_semicolon() -> int: return 0
+ func func_1() -> int: return 0
+ func func_2() -> int: return 0
+ func func_without_return_type(): pass
+
+abstract class D extends C:
+ abstract func get_text_1() -> String
+
+ func get_text_2() -> String:
+ return super() + " text_2d"
+
+class E extends D:
+ func get_text_1() -> String:
+ return "text_1e"
+
+func test():
+ var c := C.new()
+ c.print_text_1()
+ c.print_text_2()
+
+ var e := E.new()
+ e.print_text_1()
+ e.print_text_2()
diff --git a/modules/gdscript/tests/scripts/runtime/features/abstract_methods.out b/modules/gdscript/tests/scripts/runtime/features/abstract_methods.out
new file mode 100644
index 0000000000..87b9204b16
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/abstract_methods.out
@@ -0,0 +1,5 @@
+GDTEST_OK
+text_1b
+text_2c
+text_1e
+text_2c text_2d
diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd
index 4ddbeaec0b..40197650c3 100644
--- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd
+++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd
@@ -1,18 +1,12 @@
# GH-82169
-class A:
- static var test_static_var_a1
- static var test_static_var_a2
- var test_var_a1
- var test_var_a2
- static func test_static_func_a1(): pass
- static func test_static_func_a2(): pass
- func test_func_a1(): pass
- func test_func_a2(): pass
- @warning_ignore("unused_signal")
- signal test_signal_a1()
- @warning_ignore("unused_signal")
- signal test_signal_a2()
+@warning_ignore_start("unused_signal")
+
+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
class B extends A:
static var test_static_var_b1
@@ -21,27 +15,67 @@ class B extends A:
var test_var_b2
static func test_static_func_b1(): pass
static func test_static_func_b2(): pass
+ func test_abstract_func_1(): pass
+ func test_abstract_func_2(): pass
+ func test_override_func_1(): pass
+ func test_override_func_2(): pass
func test_func_b1(): pass
func test_func_b2(): pass
- @warning_ignore("unused_signal")
signal test_signal_b1()
- @warning_ignore("unused_signal")
signal test_signal_b2()
+class C extends B:
+ static var test_static_var_c1
+ static var test_static_var_c2
+ var test_var_c1
+ var test_var_c2
+ static func test_static_func_c1(): pass
+ static func test_static_func_c2(): pass
+ func test_abstract_func_1(): pass
+ func test_abstract_func_2(): pass
+ func test_override_func_1(): pass
+ func test_override_func_2(): pass
+ func test_func_c1(): pass
+ func test_func_c2(): pass
+ signal test_signal_c1()
+ signal test_signal_c2()
+
+func test_property_signature(name: String, base: Object, is_static: bool = false) -> void:
+ prints("---", name, "---")
+ for property in base.get_property_list():
+ if str(property.name).begins_with("test_"):
+ print(Utils.get_property_signature(property, null, is_static))
+
+func test_method_signature(name: String, base: Object) -> void:
+ prints("---", name, "---")
+ for method in base.get_method_list():
+ if str(method.name).begins_with("test_"):
+ print(Utils.get_method_signature(method))
+
+func test_signal_signature(name: String, base: Object) -> void:
+ prints("---", name, "---")
+ for method in base.get_signal_list():
+ if str(method.name).begins_with("test_"):
+ print(Utils.get_method_signature(method, true))
+
func test():
var b := B.new()
- for property in (B as GDScript).get_property_list():
- if str(property.name).begins_with("test_"):
- print(Utils.get_property_signature(property, null, true))
- print("---")
- for property in b.get_property_list():
- if str(property.name).begins_with("test_"):
- print(Utils.get_property_signature(property))
- print("---")
- for method in b.get_method_list():
- if str(method.name).begins_with("test_"):
- print(Utils.get_method_signature(method))
- print("---")
- for method in b.get_signal_list():
- if str(method.name).begins_with("test_"):
- print(Utils.get_method_signature(method, true))
+ var c := C.new()
+
+ print("=== Class Properties ===")
+ test_property_signature("A", A as GDScript, true)
+ test_property_signature("B", B as GDScript, true)
+ test_property_signature("C", C as GDScript, true)
+ print("=== Member Properties ===")
+ test_property_signature("B", b)
+ test_property_signature("C", c)
+ print("=== Class Methods ===")
+ test_method_signature("A", A as GDScript)
+ test_method_signature("B", B as GDScript)
+ test_method_signature("C", C as GDScript)
+ print("=== Member Methods ===")
+ test_method_signature("B", b)
+ test_method_signature("C", c)
+ print("=== Signals ===")
+ test_signal_signature("B", b)
+ test_signal_signature("C", c)
diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out
index f294b66ef9..33b3064278 100644
--- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out
+++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out
@@ -1,24 +1,68 @@
GDTEST_OK
-static var test_static_var_a1: Variant
-static var test_static_var_a2: Variant
+=== Class Properties ===
+--- A ---
+--- B ---
static var test_static_var_b1: Variant
static var test_static_var_b2: Variant
----
+--- C ---
+static var test_static_var_b1: Variant
+static var test_static_var_b2: Variant
+static var test_static_var_c1: Variant
+static var test_static_var_c2: Variant
+=== Member Properties ===
+--- B ---
var test_var_b1: Variant
var test_var_b2: Variant
-var test_var_a1: Variant
-var test_var_a2: Variant
----
+--- C ---
+var test_var_c1: Variant
+var test_var_c2: Variant
+var test_var_b1: Variant
+var test_var_b2: Variant
+=== Class Methods ===
+--- A ---
+--- B ---
+--- C ---
+=== Member Methods ===
+--- B ---
static func test_static_func_b1() -> void
static func test_static_func_b2() -> void
+func test_abstract_func_1() -> void
+func test_abstract_func_2() -> void
+func test_override_func_1() -> void
+func test_override_func_2() -> void
func test_func_b1() -> void
func test_func_b2() -> void
-static func test_static_func_a1() -> void
-static func test_static_func_a2() -> void
-func test_func_a1() -> void
-func test_func_a2() -> 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 ---
+static func test_static_func_c1() -> void
+static func test_static_func_c2() -> void
+func test_abstract_func_1() -> void
+func test_abstract_func_2() -> void
+func test_override_func_1() -> void
+func test_override_func_2() -> void
+func test_func_c1() -> void
+func test_func_c2() -> void
+static func test_static_func_b1() -> void
+static func test_static_func_b2() -> void
+func test_abstract_func_1() -> void
+func test_abstract_func_2() -> void
+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
+func test_override_func_1() -> void
+func test_override_func_2() -> void
+=== Signals ===
+--- B ---
+signal test_signal_b1()
+signal test_signal_b2()
+--- C ---
+signal test_signal_c1()
+signal test_signal_c2()
signal test_signal_b1()
signal test_signal_b2()
-signal test_signal_a1()
-signal test_signal_a2()
diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd
index 4e715e14ea..e79005c398 100644
--- a/modules/gdscript/tests/scripts/utils.notest.gd
+++ b/modules/gdscript/tests/scripts/utils.notest.gd
@@ -100,6 +100,8 @@ static func print_property_extended_info(property: Dictionary, base: Object = nu
static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String:
var result: String = ""
+ if method.flags & METHOD_FLAG_VIRTUAL_REQUIRED:
+ result += "abstract "
if method.flags & METHOD_FLAG_STATIC:
result += "static "
result += ("signal " if is_signal else "func ") + method.name + "("