Skip to content

Commit dd4696b

Browse files
Add an @override annotation for enforcing proper override semantics
1 parent 825ef23 commit dd4696b

32 files changed

+324
-6
lines changed

core/object/script_language.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,23 @@ class ScriptLanguage : public Object {
306306
LOCATION_OTHER = 1 << 10,
307307
};
308308

309+
/**
310+
* @brief Represents a position in a text editor. Uses zero-indexed line and column numbers.
311+
*/
312+
struct CodePos {
313+
int line;
314+
int column;
315+
};
316+
317+
/**
318+
* @brief Represents a replacement hunk of text for a given region based on the start and end pos.
319+
*/
320+
struct TextEdit {
321+
CodePos start;
322+
CodePos end;
323+
String new_text;
324+
};
325+
309326
struct CodeCompletionOption {
310327
CodeCompletionKind kind = CODE_COMPLETION_KIND_PLAIN_TEXT;
311328
String display;
@@ -318,6 +335,14 @@ class ScriptLanguage : public Object {
318335
int location = LOCATION_OTHER;
319336
String theme_color_name;
320337

338+
/**
339+
* @brief Additional text edits that will be applied if this suggestion is applied.
340+
* These should not modify the range that is being modified by insert_text.
341+
*
342+
* @note The caret(s) will attempt to maintain their relative positions regardless of what changes are applied here.
343+
*/
344+
Vector<TextEdit> additional_edits;
345+
321346
CodeCompletionOption() {}
322347

323348
CodeCompletionOption(const String &p_text, CodeCompletionKind p_kind, int p_location = LOCATION_OTHER, const String &p_theme_color_name = "") {

editor/gui/code_editor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ void CodeTextEditor::_complete_request() {
10471047
} else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) {
10481048
font_color = completion_comment_color;
10491049
}
1050-
text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value, e.location);
1050+
text_editor->_add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value, e.location, e.additional_edits);
10511051
}
10521052
text_editor->update_code_completion_options(forced);
10531053
}

modules/gdscript/doc_classes/@GDScript.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,30 @@
782782
[/codeblock]
783783
</description>
784784
</annotation>
785+
<annotation name="@override">
786+
<return type="void" />
787+
<description>
788+
Marks a function as being an override. Override functions must override a function defined in a parent class, otherwise an error is raised.
789+
Functions that override methods but do not have this annotation will raise a warning.
790+
[codeblock]
791+
class Shape:
792+
func draw()
793+
794+
class Circle extends Shape:
795+
@override
796+
func draw(): # No error, no warnings
797+
print("Drawing a circle.")
798+
799+
class Square extends Shape:
800+
func draw(): # Warning due to missing @override
801+
print("Drawing a square.")
802+
803+
@override
804+
func junk(): # Error as no parent defines junk()
805+
pass
806+
[/codeblock]
807+
</description>
808+
</annotation>
785809
<annotation name="@rpc">
786810
<return type="void" />
787811
<param index="0" name="mode" type="String" default="&quot;authority&quot;" />

modules/gdscript/gdscript_analyzer.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,12 +1954,52 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
19541954

19551955
push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function);
19561956
}
1957+
1958+
// Mark the function as an override if the check succeeds.
1959+
p_function->is_override = true;
1960+
1961+
// Then if we don't see the @override annotation, raise a warning.
1962+
if (!p_function->has_override_annot) {
1963+
// Resolve the name of the base class that owns the overridden for error reporting
1964+
StringName base_class_name{ "**UnknownNativeClass**" };
1965+
1966+
const GDScriptParser::DataType *cur_base_type = &base_type;
1967+
while (cur_base_type) {
1968+
if (cur_base_type->kind == GDScriptParser::DataType::Kind::NATIVE) {
1969+
MethodInfo method_info;
1970+
if (ClassDB::get_method_info(cur_base_type->native_type, function_name, &method_info)) {
1971+
base_class_name = cur_base_type->native_type;
1972+
}
1973+
break; // Break unconditionally since this method should always give us the appropriate native class name.
1974+
} else {
1975+
const GDScriptParser::ClassNode *base_class = base_type.class_type;
1976+
if (base_class->has_function(function_name)) {
1977+
if (base_class->identifier) {
1978+
base_class_name = base_class->identifier->name;
1979+
} else {
1980+
base_class_name = "**UnidentifiedClass**";
1981+
}
1982+
break;
1983+
}
1984+
cur_base_type = &base_class->base_type;
1985+
}
1986+
}
1987+
1988+
parser->push_warning(p_function, GDScriptWarning::IMPLICIT_FUNCTION_OVERRIDE, function_name, base_class_name);
1989+
}
1990+
19571991
#ifdef DEBUG_ENABLED
19581992
if (native_base != StringName()) {
19591993
parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base);
19601994
}
19611995
#endif // DEBUG_ENABLED
19621996
}
1997+
1998+
// If a function with `@override` doesn't override anything, raise an error.
1999+
if (p_function->has_override_annot && !p_function->is_override) {
2000+
push_error(vformat(R"(The function %s has the @override annotation, but does not override anything.)", function_name), p_function);
2001+
}
2002+
19632003
#endif // TOOLS_ENABLED
19642004
}
19652005

modules/gdscript/gdscript_analyzer.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,24 @@ class GDScriptAnalyzer {
128128
GDScriptParser::DataType type_from_variant(const Variant &p_value, const GDScriptParser::Node *p_source);
129129
GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false, bool p_is_readonly = false) const;
130130
GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source);
131+
132+
/**
133+
* @brief Attempts to look up the function signature for a function based on name and base type.
134+
*
135+
* @param[in] p_source The source node in context of which the signature is being resolved. Used for error reporting.
136+
* @param[in] p_is_constructor If the target function call is a constructor. In this case `p_function` is always "new".
137+
* @param[in] base_type The target type to resolve the function on. If looking for a function `SomeClass.a_function()`, this is `SomeClass`.
138+
* @param[in] p_function The name of the target function.
139+
* @param[out] r_return_type The return type of the found function.
140+
* @param[out] r_par_types A list of the parameters types of the found function, in order from left to right.
141+
* @param[out] r_default_arg_count The number of parameters which have default values.
142+
* @param[out] r_method_flags The flags of the found function.
143+
* @param[out] r_native_class If DEBUG_ENABLED=1, and the found function is a native method, the string name of the owning native class. Otherwise an empty string name.
144+
*
145+
* @return True if the function signature was successfully resolved, false otherwise. See the remaining output variables for other data.
146+
*/
131147
bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, BitField<MethodFlags> &r_method_flags, StringName *r_native_class = nullptr);
148+
132149
bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, BitField<MethodFlags> &r_method_flags);
133150
void validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call);
134151
void validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call);

modules/gdscript/gdscript_editor.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,6 +3443,66 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
34433443
r_forced = r_result.size() > 0;
34443444
}
34453445

3446+
namespace {
3447+
3448+
/**
3449+
* @brief Used as the `on_applied` callback for GDScript completions of category `COMPLETION_OVERRIDE_METHOD`.
3450+
* This method will attempt to automatically apply the `@override` annotation when using completion to generate an override.
3451+
* If the `@override` annotation is already present, nothing will occur.
3452+
*
3453+
* @param code_edit The code editor instance.
3454+
* @param caret The caret that was used for this application of the completion.
3455+
*/
3456+
Vector<ScriptLanguage::TextEdit> get_override_text_edits(const GDScriptParser::CompletionContext &ctx, const Vector<String> &code_by_line) {
3457+
int line = ctx.current_line;
3458+
// Backtrack in the current line and upwards a few lines to search for `@override`. If we don't see it, apply it inline before the `func` keyword.
3459+
bool has_override_annot = false;
3460+
int lines_traversed = 0;
3461+
int func_line = -1; // Generally speaking we expect the line with `func` to be the current line, but for sanity we'll search for it anyway.
3462+
3463+
while (!has_override_annot && lines_traversed < 10 && line >= 0) {
3464+
String text = code_by_line.get(line);
3465+
3466+
if (text.contains("func")) {
3467+
if (func_line == -1) {
3468+
func_line = line;
3469+
} else {
3470+
break; // We've hit another `func` decl. Anything in or above this line doesn't apply to this function anymore.
3471+
}
3472+
}
3473+
3474+
if (text.contains("@override")) {
3475+
has_override_annot = true;
3476+
break;
3477+
}
3478+
3479+
line--;
3480+
lines_traversed++;
3481+
}
3482+
3483+
if (!has_override_annot && func_line != -1) {
3484+
String text = code_by_line.get(func_line);
3485+
ScriptLanguage::TextEdit edit;
3486+
edit.start = { func_line - 1, 0 }; // Always start at the base of the line above the func_line
3487+
edit.end = { func_line, 0 }; // And then end on the func line itself.
3488+
3489+
// What we actually need to do here is replace whatever the line above the func_line was with "<line>\n@override\n"
3490+
// But the @override has to have the same indentation level as the func_line.
3491+
3492+
int func_column = text.length() - text.lstrip(" \t").length();
3493+
String new_text = code_by_line.get(func_line - 1);
3494+
new_text = new_text + "\n" + text.substr(0, func_column) + "@override\n";
3495+
3496+
edit.new_text = new_text;
3497+
3498+
return { edit };
3499+
}
3500+
3501+
return {};
3502+
}
3503+
3504+
} // namespace
3505+
34463506
::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path, Object *p_owner, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_forced, String &r_call_hint) {
34473507
const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\"";
34483508

@@ -3452,6 +3512,8 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
34523512
parser.parse(p_code, p_path, true);
34533513
analyzer.analyze();
34543514

3515+
Vector<String> code_by_line = p_code.split("\n");
3516+
34553517
r_forced = false;
34563518
HashMap<String, ScriptLanguage::CodeCompletionOption> options;
34573519

@@ -3704,6 +3766,10 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
37043766
String display_name = member.function->identifier->name;
37053767
display_name += member.function->signature + ":";
37063768
ScriptLanguage::CodeCompletionOption option(display_name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);
3769+
3770+
// When inserting a completion for a function override, we want to automatically add the @override annotation to the completion.
3771+
option.additional_edits = get_override_text_edits(completion_context, code_by_line);
3772+
37073773
options.insert(member.function->identifier->name, option); // Insert name instead of display to track duplicates.
37083774
}
37093775
native_type = native_type.class_type->base_type;
@@ -3778,6 +3844,10 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
37783844
method_hint += ":";
37793845

37803846
ScriptLanguage::CodeCompletionOption option(method_hint, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);
3847+
3848+
// When inserting a completion for a function override, we want to automatically add the @override annotation to the completion.
3849+
option.additional_edits = get_override_text_edits(completion_context, code_by_line);
3850+
37813851
options.insert(option.display, option);
37823852
}
37833853
} break;

modules/gdscript/gdscript_parser.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ GDScriptParser::GDScriptParser() {
9797
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
9898
register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
9999
register_annotation(MethodInfo("@abstract"), AnnotationInfo::SCRIPT | AnnotationInfo::CLASS | AnnotationInfo::FUNCTION, &GDScriptParser::abstract_annotation);
100+
register_annotation(MethodInfo("@override"), AnnotationInfo::FUNCTION, &GDScriptParser::override_annotation);
100101
// Onready annotation.
101102
register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
102103
// Export annotations.
@@ -266,6 +267,7 @@ void GDScriptParser::override_completion_context(const Node *p_for_node, Complet
266267
context.current_function = current_function;
267268
context.current_suite = current_suite;
268269
context.current_line = tokenizer->get_cursor_line();
270+
context.current_column = tokenizer->get_cursor_column();
269271
context.current_argument = p_argument;
270272
context.node = p_node;
271273
context.parser = this;
@@ -288,6 +290,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node
288290
context.current_function = current_function;
289291
context.current_suite = current_suite;
290292
context.current_line = tokenizer->get_cursor_line();
293+
context.current_column = tokenizer->get_cursor_column();
291294
context.current_argument = p_argument;
292295
context.node = p_node;
293296
context.parser = this;
@@ -310,6 +313,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Typ
310313
context.current_function = current_function;
311314
context.current_suite = current_suite;
312315
context.current_line = tokenizer->get_cursor_line();
316+
context.current_column = tokenizer->get_cursor_column();
313317
context.builtin_type = p_builtin_type;
314318
context.parser = this;
315319
if (!completion_call_stack.is_empty()) {
@@ -4397,6 +4401,20 @@ bool GDScriptParser::abstract_annotation(AnnotationNode *p_annotation, Node *p_t
43974401
ERR_FAIL_V_MSG(false, R"("@abstract" annotation can only be applied to classes and functions.)");
43984402
}
43994403

4404+
bool GDScriptParser::override_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4405+
if (p_target->type == Node::FUNCTION) {
4406+
FunctionNode *function_node = static_cast<FunctionNode *>(p_target);
4407+
if (function_node->is_static) {
4408+
push_error(R"("@override" annotation cannot be applied to static functions.)", p_annotation);
4409+
return false;
4410+
}
4411+
// The only thing we can do here is record that we have the annotation. The analyzer gets to do the rest.
4412+
function_node->has_override_annot = true;
4413+
return true;
4414+
}
4415+
ERR_FAIL_V_MSG(false, R"("@override" annotation can only be applied to functions.)");
4416+
}
4417+
44004418
bool GDScriptParser::onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
44014419
ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)");
44024420

modules/gdscript/gdscript_parser.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,15 @@ class GDScriptParser {
857857
bool is_abstract = false;
858858
bool is_static = false; // For lambdas it's determined in the analyzer.
859859
bool is_coroutine = false;
860+
861+
// If this function is known to override a function in the parent type.
862+
// If a function is an override, but does not have the @override annotation, a warning is raised.
863+
// The value of this field is undefined unless resolved_signature is true.
864+
bool is_override = false;
865+
866+
// If this function node has the @override annotation. This does not indicate if the function is actually an override or not.
867+
bool has_override_annot = false;
868+
860869
Variant rpc_config;
861870
MethodInfo info;
862871
LambdaNode *source_lambda = nullptr;
@@ -867,6 +876,7 @@ class GDScriptParser {
867876
String signature; // For autocompletion.
868877
#endif // TOOLS_ENABLED
869878

879+
// True if GDScriptAnalyzer::resolve_function_signature has been called for this function node.
870880
bool resolved_signature = false;
871881
bool resolved_body = false;
872882

@@ -1319,6 +1329,7 @@ class GDScriptParser {
13191329
FunctionNode *current_function = nullptr;
13201330
SuiteNode *current_suite = nullptr;
13211331
int current_line = -1;
1332+
int current_column = -1;
13221333
union {
13231334
int current_argument = -1;
13241335
int type_chain_index;
@@ -1529,6 +1540,7 @@ class GDScriptParser {
15291540
bool icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15301541
bool static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15311542
bool abstract_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
1543+
bool override_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15321544
bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15331545
template <PropertyHint t_hint, Variant::Type t_type>
15341546
bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);

modules/gdscript/gdscript_warning.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ String GDScriptWarning::get_message() const {
162162
return vformat(R"*(The default value uses "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]);
163163
case ONREADY_WITH_EXPORT:
164164
return R"("@onready" will set the default value after "@export" takes effect and will override it.)";
165+
case IMPLICIT_FUNCTION_OVERRIDE:
166+
CHECK_SYMBOLS(2);
167+
return vformat(R"*(The method "%s()" overrides "%s::%s()" but does not have the @override annotation.)*", symbols[0], symbols[1], symbols[0]);
165168
#ifndef DISABLE_DEPRECATED
166169
// Never produced. These warnings migrated from 3.x by mistake.
167170
case PROPERTY_USED_AS_FUNCTION: // There is already an error.
@@ -238,6 +241,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
238241
PNAME("NATIVE_METHOD_OVERRIDE"),
239242
PNAME("GET_NODE_DEFAULT_WITHOUT_ONREADY"),
240243
PNAME("ONREADY_WITH_EXPORT"),
244+
PNAME("IMPLICIT_FUNCTION_OVERRIDE"),
241245
#ifndef DISABLE_DEPRECATED
242246
"PROPERTY_USED_AS_FUNCTION",
243247
"CONSTANT_USED_AS_FUNCTION",

modules/gdscript/gdscript_warning.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class GDScriptWarning {
8989
NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended.
9090
GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation.
9191
ONREADY_WITH_EXPORT, // The `@onready` annotation will set the value after `@export` which is likely not intended.
92+
IMPLICIT_FUNCTION_OVERRIDE, // A function overrides a function in a parent class, but does not have the `@override` annotation.
9293
#ifndef DISABLE_DEPRECATED
9394
PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name.
9495
CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name.
@@ -146,6 +147,7 @@ class GDScriptWarning {
146147
ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected.
147148
ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected.
148149
ERROR, // ONREADY_WITH_EXPORT // May not work as expected.
150+
WARN, // IMPLICIT_FUNCTION_OVERRIDE
149151
#ifndef DISABLE_DEPRECATED
150152
WARN, // PROPERTY_USED_AS_FUNCTION
151153
WARN, // CONSTANT_USED_AS_FUNCTION

0 commit comments

Comments
 (0)