From a7cf2069d5acda3b39608c70cbcde55d91463a87 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Wed, 14 May 2025 22:52:19 +0300 Subject: [PATCH] GDScript: Add abstract methods Co-authored-by: ryanabx --- doc/classes/@GlobalScope.xml | 2 +- editor/editor_help.cpp | 2 + modules/gdscript/editor/gdscript_docgen.cpp | 10 +- modules/gdscript/gdscript_analyzer.cpp | 57 ++++++++- modules/gdscript/gdscript_compiler.cpp | 5 + modules/gdscript/gdscript_parser.cpp | 109 +++++++++++------- modules/gdscript/gdscript_parser.h | 3 +- .../analyzer/errors/abstract_methods.gd | 28 +++++ .../analyzer/errors/abstract_methods.out | 6 + .../common/override_function_abstract.cfg | 4 + .../common/override_function_abstract.gd | 5 + .../parser/errors/abstract_func_with_body.gd | 8 ++ .../parser/errors/abstract_func_with_body.out | 2 + .../parser/errors/abstract_static_func.gd | 8 ++ .../parser/errors/abstract_static_func.out | 2 + .../parser/errors/duplicate_abstract.out | 2 - ...bstract.gd => duplicate_abstract_class.gd} | 0 .../errors/duplicate_abstract_class.out | 2 + .../parser/errors/duplicate_abstract_func.gd | 7 ++ .../parser/errors/duplicate_abstract_func.out | 2 + .../parser/errors/static_abstract_func.gd | 8 ++ .../parser/errors/static_abstract_func.out | 2 + .../runtime/features/abstract_methods.gd | 48 ++++++++ .../runtime/features/abstract_methods.out | 5 + .../features/member_info_inheritance.gd | 94 ++++++++++----- .../features/member_info_inheritance.out | 70 ++++++++--- .../gdscript/tests/scripts/utils.notest.gd | 2 + 27 files changed, 399 insertions(+), 94 deletions(-) create mode 100644 modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.gd create mode 100644 modules/gdscript/tests/scripts/analyzer/errors/abstract_methods.out create mode 100644 modules/gdscript/tests/scripts/completion/common/override_function_abstract.cfg create mode 100644 modules/gdscript/tests/scripts/completion/common/override_function_abstract.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/abstract_func_with_body.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/abstract_static_func.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/abstract_static_func.out delete mode 100644 modules/gdscript/tests/scripts/parser/errors/duplicate_abstract.out rename modules/gdscript/tests/scripts/parser/errors/{duplicate_abstract.gd => duplicate_abstract_class.gd} (100%) create mode 100644 modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_class.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/duplicate_abstract_func.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/static_abstract_func.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/static_abstract_func.out create mode 100644 modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd create mode 100644 modules/gdscript/tests/scripts/runtime/features/abstract_methods.out 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 + "("