Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/classes/@GlobalScope.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 2 additions & 0 deletions editor/editor_help.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ");
Expand Down
10 changes: 9 additions & 1 deletion modules/gdscript/editor/gdscript_docgen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
57 changes: 51 additions & 6 deletions modules/gdscript/gdscript_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down
109 changes: 69 additions & 40 deletions modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1028,25 +1047,19 @@ 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;
switch (token.type) {
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:
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 ");
}
Expand Down
3 changes: 2 additions & 1 deletion modules/gdscript/gdscript_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,7 @@ class GDScriptParser {
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;
Expand Down Expand Up @@ -1502,7 +1503,7 @@ class GDScriptParser {
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);
Expand Down
Loading