Browse Source

GDScript: Add abstract methods

Co-authored-by: ryanabx <[email protected]>
Danil Alexeev 3 tháng trước cách đây
mục cha
commit
a7cf2069d5
27 tập tin đã thay đổi với 396 bổ sung91 xóa
  1. 1 1
      doc/classes/@GlobalScope.xml
  2. 2 0
      editor/editor_help.cpp
  3. 9 1
      modules/gdscript/editor/gdscript_docgen.cpp
  4. 51 6
      modules/gdscript/gdscript_analyzer.cpp
  5. 5 0
      modules/gdscript/gdscript_compiler.cpp
  6. 69 40
      modules/gdscript/gdscript_parser.cpp
  7. 2 1
      modules/gdscript/gdscript_parser.h
  8. 28 0
      modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd
  9. 6 0
      modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out
  10. 4 0
      modules/gdscript/tests/scripts/completion/common/override_function_abstract.cfg
  11. 5 0
      modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd
  12. 8 0
      modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd
  13. 2 0
      modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out
  14. 8 0
      modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd
  15. 2 0
      modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out
  16. 0 2
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.out
  17. 0 0
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd
  18. 2 0
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out
  19. 7 0
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd
  20. 2 0
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out
  21. 8 0
      modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd
  22. 2 0
      modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out
  23. 48 0
      modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd
  24. 5 0
      modules/gdscript/tests/scripts/runtime/features/abstract_methods.out
  25. 61 27
      modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd
  26. 57 13
      modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out
  27. 2 0
      modules/gdscript/tests/scripts/utils.notest.gd

+ 1 - 1
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.
 		</constant>
 		<constant name="METHOD_FLAG_VIRTUAL_REQUIRED" value="128" enum="MethodFlags" is_bitfield="true">
-			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.
 		</constant>
 		<constant name="METHOD_FLAGS_DEFAULT" value="1" enum="MethodFlags" is_bitfield="true">
 			Default method flags (normal).

+ 2 - 0
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(" ");

+ 9 - 1
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";

+ 51 - 6
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<StringName> 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<GDScriptParserRef> 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);
 		}

+ 5 - 0
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 = "<anonymous lambda>";
 		}
+		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;
 	}

+ 69 - 40
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<FunctionNode>();
+	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 ");
 	}

+ 2 - 1
modules/gdscript/gdscript_parser.h

@@ -853,6 +853,7 @@ public:
 		HashMap<StringName, int> 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 <typename T>
 	void parse_class_member(T *(GDScriptParser::*p_parse_function)(bool, bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_abstract = false, bool p_is_static = false);
 	SignalNode *parse_signal(bool p_is_abstract, bool p_is_static);

+ 28 - 0
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

+ 6 - 0
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.

+ 4 - 0
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:"},
+]

+ 5 - 0
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 ➡

+ 8 - 0
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

+ 2 - 0
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.

+ 8 - 0
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

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out

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

+ 0 - 2
modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.out

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

+ 0 - 0
modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.gd → modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd


+ 2 - 0
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".

+ 7 - 0
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

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out

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

+ 8 - 0
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

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out

@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected "func" or "var" after "static".

+ 48 - 0
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()

+ 5 - 0
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

+ 61 - 27
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()
 
-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():
+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))
-	print("---")
-	for method in b.get_method_list():
+			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))
-	print("---")
-	for method in b.get_signal_list():
+
+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()
+	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)

+ 57 - 13
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
+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
-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
+=== 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()

+ 2 - 0
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 + "("