Просмотр исходного кода

Merge pull request #107717 from aaronfranke/abstract-annotation

GDScript: Replace `abstract` keyword with `@abstract` annotation
Rémi Verschelde 1 месяц назад
Родитель
Сommit
ebc36a7225
31 измененных файлов с 179 добавлено и 197 удалено
  1. 21 0
      modules/gdscript/doc_classes/@GDScript.xml
  2. 1 2
      modules/gdscript/gdscript.cpp
  3. 17 3
      modules/gdscript/gdscript_analyzer.cpp
  4. 1 1
      modules/gdscript/gdscript_editor.cpp
  5. 72 95
      modules/gdscript/gdscript_parser.cpp
  6. 11 10
      modules/gdscript/gdscript_parser.h
  7. 0 4
      modules/gdscript/gdscript_tokenizer.cpp
  8. 0 1
      modules/gdscript/gdscript_tokenizer.h
  9. 18 5
      modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd
  10. 8 3
      modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out
  11. 1 1
      modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_class.gd
  12. 1 1
      modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_script.notest.gd
  13. 1 1
      modules/gdscript/tests/scripts/analyzer/features/extend_abstract_class.gd
  14. 2 2
      modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd
  15. 0 8
      modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd
  16. 0 2
      modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out
  17. 0 8
      modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd
  18. 0 2
      modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out
  19. 1 1
      modules/gdscript/tests/scripts/parser/errors/brace_syntax.out
  20. 0 7
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.gd
  21. 0 2
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out
  22. 0 7
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd
  23. 0 2
      modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out
  24. 3 0
      modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.gd
  25. 2 0
      modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.out
  26. 0 8
      modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd
  27. 0 2
      modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out
  28. 11 11
      modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd
  29. 3 3
      modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd
  30. 4 4
      modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out
  31. 1 1
      modules/gdscript/tests/scripts/utils.notest.gd

+ 21 - 0
modules/gdscript/doc_classes/@GDScript.xml

@@ -296,6 +296,27 @@
 		</constant>
 		</constant>
 	</constants>
 	</constants>
 	<annotations>
 	<annotations>
+		<annotation name="@abstract">
+			<return type="void" />
+			<description>
+				Marks a class or a method as abstract.
+				An abstract class is a class that cannot be instantiated directly. Instead, it is meant to be inherited by other classes. Attempting to instantiate an abstract class will result in an error.
+				An abstract method is a method that has no implementation. Therefore, a newline or a semicolon is expected after the function header. This defines a contract that inheriting classes must conform to, because the method signature must be compatible when overriding.
+				Inheriting classes must either provide implementations for all abstract methods, or the inheriting class must be marked as abstract. If a class has at least one abstract method (either its own or an unimplemented inherited one), then it must also be marked as abstract. However, the reverse is not true: an abstract class is allowed to have no abstract methods.
+				[codeblock]
+				@abstract class Shape:
+					@abstract func draw()
+
+				class Circle extends Shape:
+					func draw():
+						print("Drawing a circle.")
+
+				class Square extends Shape:
+					func draw():
+						print("Drawing a square.")
+				[/codeblock]
+			</description>
+		</annotation>
 		<annotation name="@export">
 		<annotation name="@export">
 			<return type="void" />
 			<return type="void" />
 			<description>
 			<description>

+ 1 - 2
modules/gdscript/gdscript.cpp

@@ -2749,7 +2749,6 @@ Vector<String> GDScriptLanguage::get_reserved_words() const {
 		"when",
 		"when",
 		"while",
 		"while",
 		// Declarations.
 		// Declarations.
-		"abstract",
 		"class",
 		"class",
 		"class_name",
 		"class_name",
 		"const",
 		"const",
@@ -2915,7 +2914,7 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b
 		*r_icon_path = c->simplified_icon_path;
 		*r_icon_path = c->simplified_icon_path;
 	}
 	}
 	if (r_is_abstract) {
 	if (r_is_abstract) {
-		*r_is_abstract = false;
+		*r_is_abstract = c->is_abstract;
 	}
 	}
 	if (r_is_tool) {
 	if (r_is_tool) {
 		*r_is_tool = parser.is_tool();
 		*r_is_tool = parser.is_tool();

+ 17 - 3
modules/gdscript/gdscript_analyzer.cpp

@@ -1540,12 +1540,12 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
 					if (member.function->is_abstract) {
 					if (member.function->is_abstract) {
 						if (base_class == p_class) {
 						if (base_class == p_class) {
 							const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name);
 							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);
+							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;
 							break;
 						} else if (!implemented_funcs.has(member.function->identifier->name)) {
 						} 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 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);
 							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);
+							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;
 							break;
 						}
 						}
 					} else {
 					} else {
@@ -1987,6 +1987,20 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun
 	}
 	}
 	p_function->resolved_body = true;
 	p_function->resolved_body = true;
 
 
+	if (p_function->is_abstract) {
+		// Abstract functions don't have a body.
+		if (!p_function->body->statements.is_empty()) {
+			push_error(R"(Abstract function cannot have a body.)", p_function->body);
+		}
+		return;
+	} else {
+		// Non-abstract functions must have a body.
+		if (p_function->body->statements.is_empty()) {
+			push_error(R"(A function must either have a ":" followed by a body, or be marked as "@abstract".)", p_function);
+			return;
+		}
+	}
+
 	GDScriptParser::FunctionNode *previous_function = parser->current_function;
 	GDScriptParser::FunctionNode *previous_function = parser->current_function;
 	parser->current_function = p_function;
 	parser->current_function = p_function;
 
 
@@ -1999,7 +2013,7 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun
 		// Use the suite inferred type if return isn't explicitly set.
 		// Use the suite inferred type if return isn't explicitly set.
 		p_function->set_datatype(p_function->body->get_datatype());
 		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)) {
 	} 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->is_abstract && !p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) {
+		if (!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);
 			push_error(R"(Not all code paths return a value.)", p_function);
 		}
 		}
 	}
 	}

+ 1 - 1
modules/gdscript/gdscript_editor.cpp

@@ -1532,7 +1532,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context
 
 
 	static const char *_keywords_with_space[] = {
 	static const char *_keywords_with_space[] = {
 		"and", "not", "or", "in", "as", "class", "class_name", "extends", "is", "func", "signal", "await",
 		"and", "not", "or", "in", "as", "class", "class_name", "extends", "is", "func", "signal", "await",
-		"const", "enum", "abstract", "static", "var", "if", "elif", "else", "for", "match", "when", "while",
+		"const", "enum", "static", "var", "if", "elif", "else", "for", "match", "when", "while",
 		nullptr
 		nullptr
 	};
 	};
 
 

+ 72 - 95
modules/gdscript/gdscript_parser.cpp

@@ -96,6 +96,7 @@ GDScriptParser::GDScriptParser() {
 		register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation);
 		register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation);
 		register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
 		register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
 		register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
 		register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
+		register_annotation(MethodInfo("@abstract"), AnnotationInfo::SCRIPT | AnnotationInfo::CLASS | AnnotationInfo::FUNCTION, &GDScriptParser::abstract_annotation);
 		// Onready annotation.
 		// Onready annotation.
 		register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
 		register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
 		// Export annotations.
 		// Export annotations.
@@ -628,7 +629,7 @@ void GDScriptParser::parse_program() {
 					annotation_stack.push_back(annotation);
 					annotation_stack.push_back(annotation);
 				} else if (annotation->applies_to(AnnotationInfo::SCRIPT)) {
 				} else if (annotation->applies_to(AnnotationInfo::SCRIPT)) {
 					PUSH_PENDING_ANNOTATIONS_TO_HEAD;
 					PUSH_PENDING_ANNOTATIONS_TO_HEAD;
-					if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon")) {
+					if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon") || annotation->name == SNAME("@static_unload")) {
 						// Some annotations need to be resolved and applied in the parser.
 						// Some annotations need to be resolved and applied in the parser.
 						// The root class is not in any class, so `head->outer == nullptr`.
 						// The root class is not in any class, so `head->outer == nullptr`.
 						annotation->apply(this, head, nullptr);
 						annotation->apply(this, head, nullptr);
@@ -675,52 +676,9 @@ void GDScriptParser::parse_program() {
 		reset_extents(head, current);
 		reset_extents(head, current);
 	}
 	}
 
 
-	bool first_is_abstract = false;
 	while (can_have_class_or_extends) {
 	while (can_have_class_or_extends) {
 		// Order here doesn't matter, but there should be only one of each at most.
 		// Order here doesn't matter, but there should be only one of each at most.
 		switch (current.type) {
 		switch (current.type) {
-			case GDScriptTokenizer::Token::ABSTRACT: {
-				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();
-
-				// A standalone "abstract" is only allowed for script-level stuff.
-				bool is_standalone = false;
-				if (current.type == GDScriptTokenizer::Token::NEWLINE) {
-					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:
 			case GDScriptTokenizer::Token::CLASS_NAME:
 				PUSH_PENDING_ANNOTATIONS_TO_HEAD;
 				PUSH_PENDING_ANNOTATIONS_TO_HEAD;
 				advance();
 				advance();
@@ -765,15 +723,23 @@ void GDScriptParser::parse_program() {
 		}
 		}
 	}
 	}
 
 
-	// When the only thing needed is the class name and the icon, we don't need to parse the hole file.
-	// It really speed up the call to GDScriptLanguage::get_global_class_name especially for large script.
+#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
+
+	for (AnnotationNode *&annotation : head->annotations) {
+		if (annotation->name == SNAME("@abstract")) {
+			// Some annotations need to be resolved and applied in the parser.
+			// The root class is not in any class, so `head->outer == nullptr`.
+			annotation->apply(this, head, nullptr);
+		}
+	}
+
+	// When the only thing needed is the class name, icon, and abstractness; we don't need to parse the whole file.
+	// It really speed up the call to `GDScriptLanguage::get_global_class_name()` especially for large script.
 	if (!parse_body) {
 	if (!parse_body) {
 		return;
 		return;
 	}
 	}
 
 
-#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD
-
-	parse_class_body(first_is_abstract, true);
+	parse_class_body(true);
 
 
 	head->end_line = current.end_line;
 	head->end_line = current.end_line;
 	head->end_column = current.end_column;
 	head->end_column = current.end_column;
@@ -876,13 +842,12 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const {
 	return false;
 	return false;
 }
 }
 
 
-GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, bool p_is_static) {
+GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
 	ClassNode *n_class = alloc_node<ClassNode>();
 	ClassNode *n_class = alloc_node<ClassNode>();
 
 
 	ClassNode *previous_class = current_class;
 	ClassNode *previous_class = current_class;
 	current_class = n_class;
 	current_class = n_class;
 	n_class->outer = previous_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".)")) {
 	if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the class name after "class".)")) {
 		n_class->identifier = parse_identifier();
 		n_class->identifier = parse_identifier();
@@ -919,7 +884,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_abstract, bool
 		end_statement("superclass");
 		end_statement("superclass");
 	}
 	}
 
 
-	parse_class_body(false, multiline);
+	parse_class_body(multiline);
 	complete_extents(n_class);
 	complete_extents(n_class);
 
 
 	if (multiline) {
 	if (multiline) {
@@ -978,7 +943,7 @@ void GDScriptParser::parse_extends() {
 }
 }
 
 
 template <typename T>
 template <typename T>
-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) {
+void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) {
 	advance();
 	advance();
 
 
 	// Consume annotations.
 	// Consume annotations.
@@ -994,7 +959,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
 		}
 		}
 	}
 	}
 
 
-	T *member = (this->*p_parse_function)(p_is_abstract, p_is_static);
+	T *member = (this->*p_parse_function)(p_is_static);
 	if (member == nullptr) {
 	if (member == nullptr) {
 		return;
 		return;
 	}
 	}
@@ -1048,23 +1013,14 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
 	}
 	}
 }
 }
 
 
-void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multiline) {
+void GDScriptParser::parse_class_body(bool p_is_multiline) {
 	bool class_end = false;
 	bool class_end = false;
-	// 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;
 	bool next_is_static = false;
 	while (!class_end && !is_at_end()) {
 	while (!class_end && !is_at_end()) {
 		GDScriptTokenizer::Token token = current;
 		GDScriptTokenizer::Token token = current;
 		switch (token.type) {
 		switch (token.type) {
-			case GDScriptTokenizer::Token::ABSTRACT: {
-				advance();
-				next_is_abstract = true;
-				if (!check(GDScriptTokenizer::Token::CLASS) && !check(GDScriptTokenizer::Token::FUNC)) {
-					push_error(R"(Expected "class" or "func" after "abstract".)");
-				}
-			} break;
 			case GDScriptTokenizer::Token::VAR:
 			case GDScriptTokenizer::Token::VAR:
-				parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", false, next_is_static);
+				parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static);
 				if (next_is_static) {
 				if (next_is_static) {
 					current_class->has_static_data = true;
 					current_class->has_static_data = true;
 				}
 				}
@@ -1076,10 +1032,10 @@ void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multil
 				parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
 				parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
 				break;
 				break;
 			case GDScriptTokenizer::Token::FUNC:
 			case GDScriptTokenizer::Token::FUNC:
-				parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_abstract, next_is_static);
+				parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_static);
 				break;
 				break;
 			case GDScriptTokenizer::Token::CLASS:
 			case GDScriptTokenizer::Token::CLASS:
-				parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class", next_is_abstract);
+				parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class");
 				break;
 				break;
 			case GDScriptTokenizer::Token::ENUM:
 			case GDScriptTokenizer::Token::ENUM:
 				parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
 				parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
@@ -1159,9 +1115,6 @@ void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multil
 				}
 				}
 				break;
 				break;
 		}
 		}
-		if (token.type != GDScriptTokenizer::Token::ABSTRACT) {
-			next_is_abstract = false;
-		}
 		if (token.type != GDScriptTokenizer::Token::STATIC) {
 		if (token.type != GDScriptTokenizer::Token::STATIC) {
 			next_is_static = false;
 			next_is_static = false;
 		}
 		}
@@ -1174,11 +1127,11 @@ void GDScriptParser::parse_class_body(bool p_first_is_abstract, bool p_is_multil
 	}
 	}
 }
 }
 
 
-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) {
+	return parse_variable(p_is_static, true);
 }
 }
 
 
-GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_abstract, bool p_is_static, bool p_allow_property) {
+GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) {
 	VariableNode *variable = alloc_node<VariableNode>();
 	VariableNode *variable = alloc_node<VariableNode>();
 
 
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
@@ -1414,7 +1367,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
 	}
 	}
 }
 }
 
 
-GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_abstract, bool p_is_static) {
+GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) {
 	ConstantNode *constant = alloc_node<ConstantNode>();
 	ConstantNode *constant = alloc_node<ConstantNode>();
 
 
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
@@ -1482,7 +1435,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() {
 	return parameter;
 	return parameter;
 }
 }
 
 
-GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_abstract, bool p_is_static) {
+GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
 	SignalNode *signal = alloc_node<SignalNode>();
 	SignalNode *signal = alloc_node<SignalNode>();
 
 
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) {
 	if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) {
@@ -1527,7 +1480,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_abstract, boo
 	return signal;
 	return signal;
 }
 }
 
 
-GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_is_static) {
+GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) {
 	EnumNode *enum_node = alloc_node<EnumNode>();
 	EnumNode *enum_node = alloc_node<EnumNode>();
 	bool named = false;
 	bool named = false;
 
 
@@ -1625,7 +1578,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_abstract, bool p_
 	return enum_node;
 	return enum_node;
 }
 }
 
 
-void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start) {
+bool GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start) {
 	if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) {
 	if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) {
 		bool default_used = false;
 		bool default_used = false;
 		do {
 		do {
@@ -1704,17 +1657,16 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
 	}
 	}
 #endif // TOOLS_ENABLED
 #endif // TOOLS_ENABLED
 
 
-	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));
+	// TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens.
+	if (p_type == "lambda") {
+		return consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after lambda declaration.)");
 	}
 	}
+	// The colon may not be present in the case of abstract functions.
+	return match(GDScriptTokenizer::Token::COLON);
 }
 }
 
 
-GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract, bool p_is_static) {
+GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
 	FunctionNode *function = alloc_node<FunctionNode>();
 	FunctionNode *function = alloc_node<FunctionNode>();
-	function->is_abstract = p_is_abstract;
 	function->is_static = p_is_static;
 	function->is_static = p_is_static;
 
 
 	make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
 	make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
@@ -1744,9 +1696,9 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract,
 	consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)");
 	consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)");
 
 
 #ifdef TOOLS_ENABLED
 #ifdef TOOLS_ENABLED
-	parse_function_signature(function, body, "function", signature_start_pos);
+	const bool has_body = parse_function_signature(function, body, "function", signature_start_pos);
 #else // !TOOLS_ENABLED
 #else // !TOOLS_ENABLED
-	parse_function_signature(function, body, "function", -1);
+	const bool has_body = parse_function_signature(function, body, "function", -1);
 #endif // TOOLS_ENABLED
 #endif // TOOLS_ENABLED
 
 
 	current_suite = previous_suite;
 	current_suite = previous_suite;
@@ -1755,7 +1707,9 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_abstract,
 	function->min_local_doc_line = previous.end_line + 1;
 	function->min_local_doc_line = previous.end_line + 1;
 #endif // TOOLS_ENABLED
 #endif // TOOLS_ENABLED
 
 
-	if (function->is_abstract) {
+	if (!has_body) {
+		// Abstract functions do not have a body.
+		end_statement("bodyless function declaration");
 		reset_extents(body, current);
 		reset_extents(body, current);
 		complete_extents(body);
 		complete_extents(body);
 		function->body = body;
 		function->body = body;
@@ -1995,11 +1949,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
 			break;
 			break;
 		case GDScriptTokenizer::Token::VAR:
 		case GDScriptTokenizer::Token::VAR:
 			advance();
 			advance();
-			result = parse_variable(false, false, false);
+			result = parse_variable(false, false);
 			break;
 			break;
 		case GDScriptTokenizer::Token::TK_CONST:
 		case GDScriptTokenizer::Token::TK_CONST:
 			advance();
 			advance();
-			result = parse_constant(false, false);
+			result = parse_constant(false);
 			break;
 			break;
 		case GDScriptTokenizer::Token::IF:
 		case GDScriptTokenizer::Token::IF:
 			advance();
 			advance();
@@ -4220,7 +4174,6 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // MATCH,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // MATCH,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // WHEN,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // WHEN,
 		// Keywords
 		// Keywords
-		{ nullptr,                                          nullptr,                                        PREC_NONE }, // ABSTRACT
 		{ nullptr,                                          &GDScriptParser::parse_cast,                 	PREC_CAST }, // AS,
 		{ nullptr,                                          &GDScriptParser::parse_cast,                 	PREC_CAST }, // AS,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // ASSERT,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // ASSERT,
 		{ &GDScriptParser::parse_await,                  	nullptr,                                        PREC_NONE }, // AWAIT,
 		{ &GDScriptParser::parse_await,                  	nullptr,                                        PREC_NONE }, // AWAIT,
@@ -4410,6 +4363,33 @@ bool GDScriptParser::static_unload_annotation(AnnotationNode *p_annotation, Node
 	return true;
 	return true;
 }
 }
 
 
+bool GDScriptParser::abstract_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
+	// NOTE: Use `p_target`, **not** `p_class`, because when `p_target` is a class then `p_class` refers to the outer class.
+	if (p_target->type == Node::CLASS) {
+		ClassNode *class_node = static_cast<ClassNode *>(p_target);
+		if (class_node->is_abstract) {
+			push_error(R"("@abstract" annotation can only be used once per class.)", p_annotation);
+			return false;
+		}
+		class_node->is_abstract = true;
+		return true;
+	}
+	if (p_target->type == Node::FUNCTION) {
+		FunctionNode *function_node = static_cast<FunctionNode *>(p_target);
+		if (function_node->is_static) {
+			push_error(R"("@abstract" annotation cannot be applied to static functions.)", p_annotation);
+			return false;
+		}
+		if (function_node->is_abstract) {
+			push_error(R"("@abstract" annotation can only be used once per function.)", p_annotation);
+			return false;
+		}
+		function_node->is_abstract = true;
+		return true;
+	}
+	ERR_FAIL_V_MSG(false, R"("@abstract" annotation can only be applied to classes and functions.)");
+}
+
 bool GDScriptParser::onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
 bool GDScriptParser::onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
 	ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)");
 	ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)");
 
 
@@ -5782,8 +5762,8 @@ void GDScriptParser::TreePrinter::print_cast(CastNode *p_cast) {
 }
 }
 
 
 void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
 void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
-	if (p_class->is_abstract) {
-		push_text("Abstract ");
+	for (const AnnotationNode *E : p_class->annotations) {
+		print_annotation(E);
 	}
 	}
 	push_text("Class ");
 	push_text("Class ");
 	if (p_class->identifier == nullptr) {
 	if (p_class->identifier == nullptr) {
@@ -5984,9 +5964,6 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const
 	for (const AnnotationNode *E : p_function->annotations) {
 	for (const AnnotationNode *E : p_function->annotations) {
 		print_annotation(E);
 		print_annotation(E);
 	}
 	}
-	if (p_function->is_abstract) {
-		push_text("Abstract ");
-	}
 	if (p_function->is_static) {
 	if (p_function->is_static) {
 		push_text("Static ");
 		push_text("Static ");
 	}
 	}

+ 11 - 10
modules/gdscript/gdscript_parser.h

@@ -1503,17 +1503,17 @@ private:
 
 
 	// Main blocks.
 	// Main blocks.
 	void parse_program();
 	void parse_program();
-	ClassNode *parse_class(bool p_is_abstract, bool p_is_static);
+	ClassNode *parse_class(bool p_is_static);
 	void parse_class_name();
 	void parse_class_name();
 	void parse_extends();
 	void parse_extends();
-	void parse_class_body(bool p_first_is_abstract, bool p_is_multiline);
+	void parse_class_body(bool p_is_multiline);
 	template <typename T>
 	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);
-	EnumNode *parse_enum(bool p_is_abstract, bool p_is_static);
+	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);
 	ParameterNode *parse_parameter();
 	ParameterNode *parse_parameter();
-	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, int p_signature_start);
+	FunctionNode *parse_function(bool p_is_static);
+	bool parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type, int p_signature_start);
 	SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
 	SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
 	// Annotations
 	// Annotations
 	AnnotationNode *parse_annotation(uint32_t p_valid_targets);
 	AnnotationNode *parse_annotation(uint32_t p_valid_targets);
@@ -1523,6 +1523,7 @@ private:
 	bool tool_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool tool_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
+	bool abstract_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	template <PropertyHint t_hint, Variant::Type t_type>
 	template <PropertyHint t_hint, Variant::Type t_type>
 	bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
@@ -1536,12 +1537,12 @@ private:
 	bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
 	// Statements.
 	// Statements.
 	Node *parse_statement();
 	Node *parse_statement();
-	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_variable(bool p_is_static);
+	VariableNode *parse_variable(bool p_is_static, bool p_allow_property);
 	VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
 	VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
 	void parse_property_getter(VariableNode *p_variable);
 	void parse_property_getter(VariableNode *p_variable);
 	void parse_property_setter(VariableNode *p_variable);
 	void parse_property_setter(VariableNode *p_variable);
-	ConstantNode *parse_constant(bool p_is_abstract, bool p_is_static);
+	ConstantNode *parse_constant(bool p_is_static);
 	AssertNode *parse_assert();
 	AssertNode *parse_assert();
 	BreakNode *parse_break();
 	BreakNode *parse_break();
 	ContinueNode *parse_continue();
 	ContinueNode *parse_continue();

+ 0 - 4
modules/gdscript/gdscript_tokenizer.cpp

@@ -101,7 +101,6 @@ static const char *token_names[] = {
 	"match", // MATCH,
 	"match", // MATCH,
 	"when", // WHEN,
 	"when", // WHEN,
 	// Keywords
 	// Keywords
-	"abstract", // ABSTRACT,
 	"as", // AS,
 	"as", // AS,
 	"assert", // ASSERT,
 	"assert", // ASSERT,
 	"await", // AWAIT,
 	"await", // AWAIT,
@@ -200,7 +199,6 @@ bool GDScriptTokenizer::Token::is_identifier() const {
 		case IDENTIFIER:
 		case IDENTIFIER:
 		case MATCH: // Used in String.match().
 		case MATCH: // Used in String.match().
 		case WHEN: // New keyword, avoid breaking existing code.
 		case WHEN: // New keyword, avoid breaking existing code.
-		case ABSTRACT:
 		// Allow constants to be treated as regular identifiers.
 		// Allow constants to be treated as regular identifiers.
 		case CONST_PI:
 		case CONST_PI:
 		case CONST_INF:
 		case CONST_INF:
@@ -216,7 +214,6 @@ bool GDScriptTokenizer::Token::is_node_name() const {
 	// This is meant to allow keywords with the $ notation, but not as general identifiers.
 	// This is meant to allow keywords with the $ notation, but not as general identifiers.
 	switch (type) {
 	switch (type) {
 		case IDENTIFIER:
 		case IDENTIFIER:
-		case ABSTRACT:
 		case AND:
 		case AND:
 		case AS:
 		case AS:
 		case ASSERT:
 		case ASSERT:
@@ -491,7 +488,6 @@ GDScriptTokenizer::Token GDScriptTokenizerText::annotation() {
 
 
 #define KEYWORDS(KEYWORD_GROUP, KEYWORD)     \
 #define KEYWORDS(KEYWORD_GROUP, KEYWORD)     \
 	KEYWORD_GROUP('a')                       \
 	KEYWORD_GROUP('a')                       \
-	KEYWORD("abstract", Token::ABSTRACT)     \
 	KEYWORD("as", Token::AS)                 \
 	KEYWORD("as", Token::AS)                 \
 	KEYWORD("and", Token::AND)               \
 	KEYWORD("and", Token::AND)               \
 	KEYWORD("assert", Token::ASSERT)         \
 	KEYWORD("assert", Token::ASSERT)         \

+ 0 - 1
modules/gdscript/gdscript_tokenizer.h

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

+ 18 - 5
modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd

@@ -1,15 +1,15 @@
-abstract class AbstractClass:
-	abstract func some_func()
+@abstract class AbstractClass:
+	@abstract func some_func()
 
 
 class ImplementedClass extends AbstractClass:
 class ImplementedClass extends AbstractClass:
 	func some_func():
 	func some_func():
 		pass
 		pass
 
 
-abstract class AbstractClassAgain extends ImplementedClass:
-	abstract func some_func()
+@abstract class AbstractClassAgain extends ImplementedClass:
+	@abstract func some_func()
 
 
 class Test1:
 class Test1:
-	abstract func some_func()
+	@abstract func some_func()
 
 
 class Test2 extends AbstractClass:
 class Test2 extends AbstractClass:
 	pass
 	pass
@@ -24,5 +24,18 @@ class Test4 extends AbstractClass:
 	func other_func():
 	func other_func():
 		super.some_func()
 		super.some_func()
 
 
+@abstract class A:
+	@abstract @abstract func abstract_dup()
+
+	# An abstract function cannot have a body.
+	@abstract func abstract_bodyful():
+		pass
+
+	# A static function cannot be marked as `@abstract`.
+	@abstract static func abstract_stat()
+
+@abstract @abstract class DuplicateAbstract:
+	pass
+
 func test():
 func test():
 	pass
 	pass

+ 8 - 3
modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out

@@ -1,6 +1,11 @@
 GDTEST_ANALYZER_ERROR
 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 37: "@abstract" annotation can only be used once per class.
+>> ERROR at line 28: "@abstract" annotation can only be used once per function.
+>> ERROR at line 35: "@abstract" annotation cannot be applied to static functions.
+>> 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 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.
 >> ERROR at line 25: Cannot call the parent class' abstract function "some_func()" because it hasn't been defined.
+>> ERROR at line 32: Abstract function cannot have a body.
+>> ERROR at line 35: A function must either have a ":" followed by a body, or be marked as "@abstract".

+ 1 - 1
modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_class.gd

@@ -2,7 +2,7 @@ extends RefCounted
 
 
 const AbstractScript = preload("./construct_abstract_script.notest.gd")
 const AbstractScript = preload("./construct_abstract_script.notest.gd")
 
 
-abstract class AbstractClass:
+@abstract class AbstractClass:
 	pass
 	pass
 
 
 func test():
 func test():

+ 1 - 1
modules/gdscript/tests/scripts/analyzer/errors/construct_abstract_script.notest.gd

@@ -1 +1 @@
-abstract class_name AbstractScript
+@abstract class_name AbstractScript

+ 1 - 1
modules/gdscript/tests/scripts/analyzer/features/extend_abstract_class.gd

@@ -8,7 +8,7 @@ class B extends A:
 class C extends CanvasItem:
 class C extends CanvasItem:
 	pass
 	pass
 
 
-abstract class X:
+@abstract class X:
 	pass
 	pass
 
 
 class Y extends X:
 class Y extends X:

+ 2 - 2
modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd

@@ -1,5 +1,5 @@
-abstract class A:
-	abstract func test(x: int) -> void
+@abstract class A:
+	@abstract func test(x: int) -> void
 
 
 class B extends A:
 class B extends A:
 	func ➡
 	func ➡

+ 0 - 8
modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd

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

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

@@ -1,2 +0,0 @@
-GDTEST_PARSER_ERROR
-Expected end of statement after abstract function declaration, found ":" instead.

+ 0 - 8
modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd

@@ -1,8 +0,0 @@
-extends RefCounted
-
-abstract class A:
-	# Currently, an abstract function cannot be static.
-	abstract static func f()
-
-func test():
-	pass

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

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

+ 1 - 1
modules/gdscript/tests/scripts/parser/errors/brace_syntax.out

@@ -1,2 +1,2 @@
 GDTEST_PARSER_ERROR
 GDTEST_PARSER_ERROR
-Expected ":" after function declaration.
+Expected end of statement after bodyless function declaration, found "{" instead.

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

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

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

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

+ 0 - 7
modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd

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

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

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

+ 3 - 0
modules/gdscript/tests/scripts/parser/errors/lambda_without_colon.gd

@@ -0,0 +1,3 @@
+#           No colon --v
+var f = func () -> void
+	pass

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

@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected ":" after lambda declaration.

+ 0 - 8
modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd

@@ -1,8 +0,0 @@
-extends RefCounted
-
-abstract class A:
-	# Currently, an abstract function cannot be static.
-	static abstract func f()
-
-func test():
-	pass

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

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

+ 11 - 11
modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd

@@ -1,18 +1,18 @@
-abstract class A:
-	abstract func get_text_1() -> String
-	abstract func get_text_2() -> String
+@abstract class A:
+	@abstract func get_text_1() -> String
+	@abstract func get_text_2() -> String
 
 
 	# No `UNUSED_PARAMETER` warning.
 	# No `UNUSED_PARAMETER` warning.
-	abstract func func_with_param(param: int) -> int
-	abstract func func_with_rest_param(...args: Array) -> int
-	abstract func func_with_semicolon() -> int;
-	abstract func func_1() -> int; abstract func func_2() -> int
-	abstract func func_without_return_type()
+	@abstract func func_with_param(param: int) -> int
+	@abstract func func_with_rest_param(...args: Array) -> 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:
 	func print_text_1() -> void:
 		print(get_text_1())
 		print(get_text_1())
 
 
-abstract class B extends A:
+@abstract class B extends A:
 	func get_text_1() -> String:
 	func get_text_1() -> String:
 		return "text_1b"
 		return "text_1b"
 
 
@@ -30,8 +30,8 @@ class C extends B:
 	func func_2() -> int: return 0
 	func func_2() -> int: return 0
 	func func_without_return_type(): pass
 	func func_without_return_type(): pass
 
 
-abstract class D extends C:
-	abstract func get_text_1() -> String
+@abstract class D extends C:
+	@abstract func get_text_1() -> String
 
 
 	func get_text_2() -> String:
 	func get_text_2() -> String:
 		return super() + " text_2d"
 		return super() + " text_2d"

+ 3 - 3
modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd

@@ -2,9 +2,9 @@
 
 
 @warning_ignore_start("unused_signal")
 @warning_ignore_start("unused_signal")
 
 
-abstract class A:
-	abstract func test_abstract_func_1()
-	abstract func test_abstract_func_2()
+@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_1(): pass
 	func test_override_func_2(): pass
 	func test_override_func_2(): pass
 
 

+ 4 - 4
modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.out

@@ -32,8 +32,8 @@ func test_override_func_1() -> void
 func test_override_func_2() -> void
 func test_override_func_2() -> void
 func test_func_b1() -> void
 func test_func_b1() -> void
 func test_func_b2() -> void
 func test_func_b2() -> void
-abstract func test_abstract_func_1() -> void
-abstract func test_abstract_func_2() -> 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_1() -> void
 func test_override_func_2() -> void
 func test_override_func_2() -> void
 --- C ---
 --- C ---
@@ -53,8 +53,8 @@ func test_override_func_1() -> void
 func test_override_func_2() -> void
 func test_override_func_2() -> void
 func test_func_b1() -> void
 func test_func_b1() -> void
 func test_func_b2() -> void
 func test_func_b2() -> void
-abstract func test_abstract_func_1() -> void
-abstract func test_abstract_func_2() -> 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_1() -> void
 func test_override_func_2() -> void
 func test_override_func_2() -> void
 === Signals ===
 === Signals ===

+ 1 - 1
modules/gdscript/tests/scripts/utils.notest.gd

@@ -127,7 +127,7 @@ static func print_property_extended_info(
 static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String:
 static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String:
 	var result: String = ""
 	var result: String = ""
 	if method.flags & METHOD_FLAG_VIRTUAL_REQUIRED:
 	if method.flags & METHOD_FLAG_VIRTUAL_REQUIRED:
-		result += "abstract "
+		result += "@abstract "
 	if method.flags & METHOD_FLAG_STATIC:
 	if method.flags & METHOD_FLAG_STATIC:
 		result += "static "
 		result += "static "
 	result += ("signal " if is_signal else "func ") + method.name + "("
 	result += ("signal " if is_signal else "func ") + method.name + "("