GDScript: Add abstract methods

Co-authored-by: ryanabx <ryanbrue@hotmail.com>
This commit is contained in:
Danil Alexeev
2025-05-14 22:52:19 +03:00
parent 8f87e60307
commit a7cf2069d5
27 changed files with 399 additions and 94 deletions

View File

@ -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).

View File

@ -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(" ");

View File

@ -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";

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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 ");
}

View File

@ -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);

View File

@ -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

View File

@ -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.

View File

@ -0,0 +1,4 @@
[output]
include=[
{"display": "test(x: int) -> void:", "insert_text": "test(x: int) -> void:"},
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -0,0 +1,5 @@
GDTEST_OK
text_1b
text_2c
text_1e
text_2c text_2d

View File

@ -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)

View File

@ -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()

View File

@ -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 + "("